diff --git a/BenchmarkDotNet.slnx b/BenchmarkDotNet.slnx
index 9761f9f8cc..f9557849db 100644
--- a/BenchmarkDotNet.slnx
+++ b/BenchmarkDotNet.slnx
@@ -2,6 +2,7 @@
+
diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
index 12f7531cff..df56308025 100644
--- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
@@ -124,12 +124,14 @@ public void BuildProjectSilent(FilePath projectFile)
public void BuildAnalyzers()
{
context.Information("BuildSystemProvider: " + context.BuildSystem().Provider);
- string[] mccVersions = ["2.8", "3.8", "4.8"];
+ // Each band must restore as well as build: the PackageReference is chosen by MccVersion, so reusing the
+ // default band's project.assets.json silently compiles every band against the default's Roslyn.
+ string[] mccVersions = ["2.8", "3.0", "3.8", "4.4", "4.8", "4.12"];
foreach (string version in mccVersions)
{
context.DotNetBuild(context.AnalyzersProjectFile.FullPath, new DotNetBuildSettings
{
- NoRestore = true,
+ NoRestore = false,
DiagnosticOutput = true,
MSBuildSettings = context.MsBuildSettingsBuild,
Configuration = context.BuildConfiguration,
@@ -139,7 +141,7 @@ public void BuildAnalyzers()
context.DotNetBuild(context.CodeFixersProjectFile.FullPath, new DotNetBuildSettings
{
- NoRestore = true,
+ NoRestore = false,
DiagnosticOutput = true,
MSBuildSettings = context.MsBuildSettingsBuild,
Configuration = context.BuildConfiguration,
diff --git a/build/roslynBands.props b/build/roslynBands.props
new file mode 100644
index 0000000000..07375d64ef
--- /dev/null
+++ b/build/roslynBands.props
@@ -0,0 +1,26 @@
+
+
+
+ netstandard2.0
+ false
+ true
+ $(NoWarn);CS1591
+
+ 4.12
+ bin\$(Configuration)\roslyn$(MccVersion)\cs
+ false
+
+
+
+ $(DefineConstants);CODE_ANALYSIS_3_0
+
+ $(DefineConstants);CODE_ANALYSIS_3_8
+
+ $(DefineConstants);CODE_ANALYSIS_4_4
+
+ $(DefineConstants);CODE_ANALYSIS_4_8
+
+ $(DefineConstants);CODE_ANALYSIS_4_12
+ $(MccVersion).0
+
+
diff --git a/docs/articles/features/parameterization.md b/docs/articles/features/parameterization.md
index eecfd29178..cbf03da4a5 100644
--- a/docs/articles/features/parameterization.md
+++ b/docs/articles/features/parameterization.md
@@ -21,4 +21,4 @@ name: Benchmark Parameterization
[!include[IntroArrayParam](../samples/IntroArrayParam.md)]
-[!include[IntroArguments](../samples/IntroArgumentsPriority.md)]
+[!include[IntroArgumentsPriority](../samples/IntroArgumentsPriority.md)]
diff --git a/docs/articles/samples/IntroArgumentsSource.md b/docs/articles/samples/IntroArgumentsSource.md
index 62340b3a33..3ab707e127 100644
--- a/docs/articles/samples/IntroArgumentsSource.md
+++ b/docs/articles/samples/IntroArgumentsSource.md
@@ -10,9 +10,18 @@ In case you want to use a lot of values, you should use
You can mark one or several fields or properties in your class by the
[`[ArgumentsSource]`](xref:BenchmarkDotNet.Attributes.ArgumentsSourceAttribute) attribute.
In this attribute, you have to specify the name of public method/property which is going to provide the values
- (something that implements `IEnumerable`).
+ (something that implements `IEnumerable` or `IAsyncEnumerable`).
+The element type has to be named: a source declared to return only the non-generic `IEnumerable` is rejected,
+ because the generated code has nothing to infer the argument's type from.
The source may be instance or static. If the source is not in the same type as the benchmark, the type containing the source must be specified in the attribute constructor.
+A source returning `IAsyncEnumerable` is awaited while the values are read, so they can be produced
+ asynchronously without resorting to blocking sync-over-async in the source, and such a source method may take
+ an optional [`[EnumeratorCancellation]`](xref:System.Runtime.CompilerServices.EnumeratorCancellationAttribute)
+ `CancellationToken` parameter. Starting the run from a thread that carries a single-threaded
+ `SynchronizationContext` needs the asynchronous entry points - see
+ @BenchmarkDotNet.Samples.IntroParamsSource, where the same applies to `[ParamsSource]`.
+
### Source code
[!code-csharp[IntroArgumentsSource.cs](../../../samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs)]
diff --git a/docs/articles/samples/IntroParamsSource.md b/docs/articles/samples/IntroParamsSource.md
index e631fbf1db..29ca62c340 100644
--- a/docs/articles/samples/IntroParamsSource.md
+++ b/docs/articles/samples/IntroParamsSource.md
@@ -7,10 +7,29 @@ uid: BenchmarkDotNet.Samples.IntroParamsSource
In case you want to use a lot of values, you should use
[`[ParamsSource]`](xref:BenchmarkDotNet.Attributes.ParamsSourceAttribute)
You can mark one or several fields or properties in your class by the
- [`[Params]`](xref:BenchmarkDotNet.Attributes.ParamsAttribute) attribute.
+ [`[ParamsSource]`](xref:BenchmarkDotNet.Attributes.ParamsSourceAttribute) attribute.
In this attribute, you have to specify the name of public method/property which is going to provide the values
- (something that implements `IEnumerable`).
+ (something that implements `IEnumerable` or `IAsyncEnumerable`).
+The element type has to be named: a source declared to return only the non-generic `IEnumerable` is rejected,
+ because the generated code has nothing to infer the parameter's type from.
The source may be instance or static. If the source is not in the same type as the benchmark, the type containing the source must be specified in the attribute constructor.
+A static source declared on a base type is used just as one declared on the benchmark type itself.
+
+A source returning `IAsyncEnumerable` is awaited while the values are read, so they can be produced
+ asynchronously - loaded from a database or a remote service, say - without resorting to blocking
+ sync-over-async in the source. Such a source method may take an optional
+ [`[EnumeratorCancellation]`](xref:System.Runtime.CompilerServices.EnumeratorCancellationAttribute)
+ `CancellationToken` parameter, which receives the benchmark's cancellation token while the values are
+ enumerated, so the asynchronous work can be cancelled.
+
+If you start the run from a thread that carries a single-threaded `SynchronizationContext` - a WPF or
+ WinForms UI thread, or legacy ASP.NET - use the asynchronous entry points
+ ([`BenchmarkRunner.RunAsync`](xref:BenchmarkDotNet.Running.BenchmarkRunner) or
+ [`BenchmarkConverter.TypeToBenchmarksAsync`](xref:BenchmarkDotNet.Running.BenchmarkConverter)) and await
+ them. The synchronous ones block the calling thread while the values are read, so an `await` inside your
+ own source captures that context and its continuation cannot run until the call it is blocking returns.
+ Awaiting the asynchronous entry point leaves the thread free to run it. Writing the source's own awaits as
+ `ConfigureAwait(false)` avoids the capture as well.
### Source code
@@ -29,10 +48,7 @@ The source may be instance or static. If the source is not in the same type as t
### Remarks
-**A remark about IParam.**
-
-You don't need to use `IParam` anymore since `0.11.0`.
-Just use complex types as you wish and override `ToString` method to change the display names used in the results.
+Use complex types as you wish and override the `ToString` method to change the display names used in the results.
### Links
diff --git a/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs b/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs
index 273c1994a3..971c4d081c 100644
--- a/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs
@@ -1,3 +1,4 @@
+using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
namespace BenchmarkDotNet.Samples
@@ -19,6 +20,21 @@ public class IntroArgumentsSource
[Benchmark]
[ArgumentsSource(typeof(BenchmarkArguments), nameof(BenchmarkArguments.TimeSpans))] // when the arguments come from a different type, specify that type here
public void SingleArgument(TimeSpan time) => Thread.Sleep(time);
+
+ [Benchmark]
+ [ArgumentsSource(nameof(NumbersAsync))]
+ public double AsyncSourcedArguments(double x, double y) => Math.Pow(x, y);
+
+ // the source may be an IAsyncEnumerable, which BenchmarkDotNet awaits, so the values can be produced
+ // asynchronously without resorting to blocking sync-over-async in the source. It may take an optional
+ // [EnumeratorCancellation] CancellationToken, which receives the benchmark's cancellation token while
+ // the values are enumerated.
+ public static async IAsyncEnumerable NumbersAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(10, cancellationToken);
+ yield return new object[] { 1.0, 1.0 };
+ yield return new object[] { 2.0, 2.0 };
+ }
}
public static class BenchmarkArguments
diff --git a/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs b/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs
index 412728c226..64c7b8d6e1 100644
--- a/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs
+++ b/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs
@@ -1,3 +1,4 @@
+using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
namespace BenchmarkDotNet.Samples
@@ -22,8 +23,23 @@ public class IntroParamsSource
[ParamsSource(typeof(ParamsValues), nameof(ParamsValues.ValuesForC))]
public int C;
+ // public field getting its params from an asynchronous source, which BenchmarkDotNet awaits.
+ // Useful when the values can only be produced asynchronously - loaded from a database or a remote
+ // service - without resorting to blocking sync-over-async in the source.
+ [ParamsSource(nameof(ValuesForD))]
+ public int D;
+
+ // the source method may take an optional [EnumeratorCancellation] CancellationToken. It receives the
+ // benchmark's cancellation token while the values are enumerated, so the async work can be cancelled.
+ public static async IAsyncEnumerable ValuesForD([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(10, cancellationToken);
+ yield return 1;
+ yield return 2;
+ }
+
[Benchmark]
- public void Benchmark() => Thread.Sleep(A + B + C + 5);
+ public void Benchmark() => Thread.Sleep(A + B + C + D + 5);
}
public static class ParamsValues
diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
index 4351a38281..3a5450691c 100644
--- a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
+++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
@@ -13,9 +13,188 @@ internal static class AnalyzerHelper
public static LocalizableResourceString GetResourceString(string name)
=> new(name, BenchmarkDotNetAnalyzerResources.ResourceManager, typeof(BenchmarkDotNetAnalyzerResources));
+ // Shared by the [ParamsSource] and [ArgumentsSource] analyzers: the runtime only invokes a source method whose
+ // parameters are all optional, so a method with a required parameter isn't recognized as a source.
+ public static readonly DiagnosticDescriptor SourceMethodMustNotHaveRequiredParametersRule = new(
+ DiagnosticIds.General_Source_MethodMustNotHaveRequiredParameters,
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotHaveRequiredParameters_Title)),
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotHaveRequiredParameters_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotHaveRequiredParameters_Description)));
+
+ // Shared by the [ParamsSource] and [ArgumentsSource] analyzers: the runtime invokes a source method directly,
+ // and nothing supplies a generic one's type arguments.
+ public static readonly DiagnosticDescriptor SourceMethodMustNotBeGenericRule = new(
+ DiagnosticIds.General_Source_MethodMustNotBeGeneric,
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotBeGeneric_Title)),
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotBeGeneric_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotBeGeneric_Description)));
+
+ // Shared by both source analyzers: discovery reads a source's values into an object[], and a ref struct cannot
+ // be boxed. Expressible since .NET 10 gave IEnumerable an allows-ref-struct type parameter.
+ public static readonly DiagnosticDescriptor SourceElementMustNotBeByRefLikeRule = new(
+ DiagnosticIds.General_Source_ElementMustNotBeByRefLike,
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMustNotBeByRefLike_Title)),
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMustNotBeByRefLike_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMustNotBeByRefLike_Description)));
+
+ // The constraint case, which the compiler cannot decide: a type argument that is not by-ref-like reads
+ // normally, so this warns where the rule above - a ref struct the compiler can see - is an error.
+ public static readonly DiagnosticDescriptor SourceElementMayBeByRefLikeRule = new(
+ DiagnosticIds.General_Source_ElementMayBeByRefLike,
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMayBeByRefLike_Title)),
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMayBeByRefLike_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMayBeByRefLike_Description)));
+
+ // Shared by both source analyzers: a source that is both shapes cannot be read unambiguously - discovery
+ // takes the synchronous path while the generated code cannot pick a GetParameterAsync overload.
+ public static readonly DiagnosticDescriptor SourceMustNotBeAmbiguouslyEnumerableRule = new(
+ DiagnosticIds.General_Source_AmbiguousEnumerableShape,
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_AmbiguousEnumerableShape_Title)),
+ GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_AmbiguousEnumerableShape_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_AmbiguousEnumerableShape_Description)));
+
+ // Mirrors GetValidValuesForParamsSourceAsync, which passes over generic methods. True only when a public
+ // generic method of that name exists and nothing else can serve it - the runtime would have nothing to invoke.
+ public static bool SourceResolvesOnlyToGenericMethod(ITypeSymbol type, string name)
+ {
+ bool anyGenericMethod = false;
+ bool anyInvocableMethod = false;
+ bool anyReadableProperty = false;
+
+ for (ITypeSymbol? current = type; current != null; current = current.BaseType)
+ {
+ foreach (var member in current.GetMembers(name))
+ {
+ if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary && method.DeclaredAccessibility == Accessibility.Public)
+ {
+ if (method.IsGenericMethod)
+ {
+ anyGenericMethod = true;
+ }
+ else if (method.Parameters.All(parameter => parameter.IsOptional))
+ {
+ anyInvocableMethod = true;
+ }
+ }
+ else if (member is IPropertySymbol property && property.GetMethod?.DeclaredAccessibility == Accessibility.Public)
+ {
+ anyReadableProperty = true;
+ }
+ }
+ }
+
+ return anyGenericMethod && !anyInvocableMethod && !anyReadableProperty;
+ }
+
+ // Mirrors GetValidValuesForParamsSourceAsync: a public all-optional method, else a property with a public
+ // getter. True only when every candidate of that name has required parameters, bases included.
+ public static bool SourceResolvesOnlyToRequiredParameterMethod(ITypeSymbol type, string name)
+ {
+ bool anyPublicMethod = false;
+ bool anyAllOptionalMethod = false;
+ bool anyReadableProperty = false;
+
+ for (ITypeSymbol? current = type; current != null; current = current.BaseType)
+ {
+ foreach (var member in current.GetMembers(name))
+ {
+ if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary && method.DeclaredAccessibility == Accessibility.Public)
+ {
+ anyPublicMethod = true;
+ if (method.Parameters.All(parameter => parameter.IsOptional))
+ {
+ anyAllOptionalMethod = true;
+ }
+ }
+ else if (member is IPropertySymbol property && property.GetMethod?.DeclaredAccessibility == Accessibility.Public)
+ {
+ anyReadableProperty = true;
+ }
+ }
+ }
+
+ return anyPublicMethod && !anyAllOptionalMethod && !anyReadableProperty;
+ }
+
public static INamedTypeSymbol? GetBenchmarkAttributeTypeSymbol(Compilation compilation)
=> compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.BenchmarkAttribute");
+ ///
+ /// Whether a value of this type can be by-ref-like: one that is, or a type parameter whose constraint admits
+ /// one. An open declaration is judged on what every substitution guarantees, so a constraint that admits a ref
+ /// struct is answered like a ref struct. A compiler that cannot express the constraint cannot be given a
+ /// declaration carrying it either, which is what the older targets fall back to.
+ ///
+ public static bool MayBeRefLike(ITypeSymbol type)
+ {
+#if CODE_ANALYSIS_4_12
+ if (type is ITypeParameterSymbol { AllowsRefLikeType: true })
+ {
+ return true;
+ }
+#endif
+ return IsRefLikeType(type);
+ }
+
+ // ref structs are C# 7.2, but no public symbol carries IsRefLikeType until Roslyn 3.0; the oldest band reads
+ // the internal property behind it instead.
+ private static bool IsRefLikeType(ITypeSymbol type)
+#if CODE_ANALYSIS_3_0
+ => type.IsRefLikeType;
+#else
+ => RefLikeTypePolyfill.IsRefLikeType(type);
+#endif
+
+ ///
+ /// Names which of the two found, so a message reads the same from either source analyzer.
+ ///
+ public static string ByRefLikeClause(ITypeSymbol type)
+ => IsRefLikeType(type) ? "is a ref struct" : "admits a ref struct";
+
+ ///
+ /// Which of the two rules applies. A ref struct is one the compiler can see, so the source cannot work; a
+ /// constraint that merely admits one is decided by the type argument, and one that is not by-ref-like reads
+ /// perfectly well - so the two are separate ids, configurable apart.
+ ///
+ public static DiagnosticDescriptor ByRefLikeRule(ITypeSymbol type)
+ => IsRefLikeType(type) ? SourceElementMustNotBeByRefLikeRule : SourceElementMayBeByRefLikeRule;
+
+ ///
+ /// Whether is or derives from it. BenchmarkDotNet resolves
+ /// its attributes with Type.GetCustomAttributes, which matches derived attribute types, so an analyzer comparing
+ /// for exact identity would disagree with the runtime about e.g. a user's `MyParamsAttribute : ParamsAttribute`.
+ ///
+ public static bool IsOrDerivesFrom(ITypeSymbol? type, INamedTypeSymbol? baseType)
+ {
+ if (type == null || baseType == null)
+ {
+ return false;
+ }
+ for (ITypeSymbol? current = type; current != null; current = current.BaseType)
+ {
+ if (SymbolEqualityComparer.Default.Equals(current, baseType))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTypeSymbol, SyntaxList attributeLists, SemanticModel semanticModel)
{
if (attributeTypeSymbol == null || attributeTypeSymbol.TypeKind == TypeKind.Error)
@@ -33,7 +212,7 @@ public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTyp
continue;
}
- if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, attributeTypeSymbol))
+ if (IsOrDerivesFrom(attributeSyntaxTypeSymbol, attributeTypeSymbol))
{
return true;
}
@@ -53,7 +232,7 @@ public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTyp
return false;
}
- return attributeList.Any(ad => SymbolEqualityComparer.Default.Equals(ad.AttributeClass, attributeTypeSymbol));
+ return attributeList.Any(ad => IsOrDerivesFrom(ad.AttributeClass, attributeTypeSymbol));
}
public static ImmutableArray GetAttributes(string attributeName, Compilation compilation, SyntaxList attributeLists, SemanticModel semanticModel)
@@ -78,7 +257,7 @@ public static ImmutableArray GetAttributes(INamedTypeSymbol? at
continue;
}
- if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, attributeTypeSymbol))
+ if (IsOrDerivesFrom(attributeSyntaxTypeSymbol, attributeTypeSymbol))
{
attributesBuilder.Add(attributeSyntax);
}
@@ -118,6 +297,84 @@ public static Location GetLocation(this AttributeData attributeData)
=> attributeData.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attributeData.ApplicationSyntaxReference.Span)
?? Location.None;
+ ///
+ /// The source-name argument of a [ParamsSource]/[ArgumentsSource] usage - nameof(Values) in
+ /// [ArgumentsSource(nameof(Values))] , or the second argument of the typeof-qualified form. Every rule
+ /// about the named source reports here, so the squiggle sits on the member to change rather than on the whole
+ /// attribute. Falls back to the attribute when the argument cannot be located.
+ ///
+ public static Location GetSourceNameLocation(this AttributeData attributeData)
+ {
+ if (attributeData.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax { ArgumentList: { } argumentList })
+ {
+ // By name first: ConstructorArguments is in parameter order and the syntax list in source order, which
+ // coincide only while every argument is positional. A named one may sit anywhere.
+ foreach (var argument in argumentList.Arguments)
+ {
+ if (argument.NameColon?.Name.Identifier.ValueText == "name")
+ {
+ return argument.Expression.GetLocation();
+ }
+ }
+
+ // All positional, so the name is the last argument - after the type where one is given.
+ int nameIndex = attributeData.ConstructorArguments.Length == 2 ? 1 : 0;
+ if (argumentList.Arguments.Count > nameIndex && argumentList.Arguments[nameIndex].NameColon is null)
+ {
+ return argumentList.Arguments[nameIndex].Expression.GetLocation();
+ }
+ }
+ return attributeData.GetLocation();
+ }
+
+ ///
+ /// Finds a [ParamsSource]/[ArgumentsSource] source member (method or property) by name, searching the type
+ /// and its base types. Mirrors the runtime resolution, which uses GetAllMethods/GetAllProperties (inherited
+ /// members included), unlike ITypeSymbol.GetMembers which only returns declared members.
+ ///
+ public static ISymbol? FindSourceMember(ITypeSymbol type, string name)
+ {
+ ISymbol? readableProperty = null;
+ ISymbol? writeOnlyProperty = null;
+ ISymbol? otherMethod = null;
+
+ for (ITypeSymbol? current = type; current != null; current = current.BaseType)
+ {
+ foreach (var member in current.GetMembers(name))
+ {
+ switch (member)
+ {
+ // What the runtime invokes: the first public non-generic method whose parameters are all
+ // optional. A generic overload is passed over here as it is there, so the rules that read the
+ // source's return type read the one that will actually be called.
+ case IMethodSymbol { MethodKind: MethodKind.Ordinary, DeclaredAccessibility: Accessibility.Public, IsGenericMethod: false } method
+ when method.Parameters.All(parameter => parameter.IsOptional):
+ return method;
+
+ // The runtime falls back to a property with a public getter, so one of those wins wherever it is
+ // found - a write-only property nearer the derived end does not hide it, as taking the first of
+ // either kind would have made it do.
+ case IPropertySymbol { GetMethod.DeclaredAccessibility: Accessibility.Public }:
+ readableProperty ??= member;
+ break;
+
+ // A write-only property is nothing the runtime would read, but it is still the member the name
+ // most likely meant, so it is returned when nothing better turns up and BDN1305 reports it.
+ case IPropertySymbol:
+ writeOnlyProperty ??= member;
+ break;
+
+ // Nothing the runtime would use, but reporting on it beats reporting nothing.
+ case IMethodSymbol { MethodKind: MethodKind.Ordinary }:
+ otherMethod ??= member;
+ break;
+ }
+ }
+ }
+
+ return readableProperty ?? writeOnlyProperty ?? otherMethod;
+ }
+
public static bool IsAssignable(TypedConstant constant, ExpressionSyntax expression, ITypeSymbol targetType, Compilation compilation)
{
if (constant.IsNull)
diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md
index 3c1d68169b..6eeeebd209 100644
--- a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md
@@ -2,7 +2,17 @@
Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
-BDN1305 | Usage | Error | [ParamsSource] cannot reference write-only property
+BDN1109 | Usage | Error | Required member cannot be set by BenchmarkDotNet
+BDN1110 | Usage | Error | Benchmark constructor must not be annotated with [SetsRequiredMembers]
+BDN1208 | Usage | Error | [Params(Source\|AllValues)] member name is reserved by code generation
+BDN1305 | Usage | Error | [ParamsSource] cannot reference write-only property
+BDN1306 | Usage | Error | [ParamsSource] must return a generic enumerable or async enumerable
+BDN1307 | Usage | Error | [ParamsSource]/[ArgumentsSource] source method must not have required parameters
+BDN1308 | Usage | Error | [ParamsSource]/[ArgumentsSource] source must not have more than one enumerable shape
+BDN1310 | Usage | Error | [ParamsSource]/[ArgumentsSource] source method must not be generic
+BDN1311 | Usage | Error | [ParamsSource]/[ArgumentsSource] source must not yield a ref struct
+BDN1312 | Usage | Warning | [ParamsSource]/[ArgumentsSource] source may yield a ref struct
+BDN1504 | Usage | Error | [ArgumentsSource] must return a generic enumerable or async enumerable
BDN1600 | Usage | Error | Fields or properties annotated with [BenchmarkCancellation] must be of type CancellationToken
BDN1601 | Usage | Error | Fields annotated with [BenchmarkCancellation] must be public
BDN1602 | Usage | Error | Properties annotated with [BenchmarkCancellation] must be public
@@ -18,4 +28,5 @@ BDN1800 | Usage | Warning | Setting both Runtime and Toolchain on a job is
Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
-BDN1100 | Usage | Error | Rule removed as GenericTypeArguments now supports abstract classes
\ No newline at end of file
+BDN1100 | Usage | Error | Rule removed as GenericTypeArguments now supports abstract classes
+BDN1206 | Usage | Error | Rule removed as parameters are now assigned through an object initializer, which can set init-only properties
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs b/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs
index bce65bfab4..741a56a466 100644
--- a/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs
+++ b/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Linq;
using Microsoft.CodeAnalysis;
namespace BenchmarkDotNet.Analyzers;
@@ -50,6 +52,87 @@ public static bool IsAsyncEnumerable(ITypeSymbol type, INamedTypeSymbol? asyncEn
return false;
}
+ ///
+ /// Whether a [ParamsSource]/[ArgumentsSource] member's return type offers no usable source shape at all -
+ /// neither IEnumerable<T> nor IAsyncEnumerable<T> . The non-generic
+ /// System.Collections.IEnumerable does not qualify on its own: the generated extraction call infers its
+ /// element type from the source, and a type with no generic instantiation gives inference nothing to bind to.
+ /// The await-foreach pattern without the IAsyncEnumerable<T> interface is not supported either.
+ ///
+ public static bool IsSupportedSourceReturnType(Compilation compilation, ITypeSymbol returnType)
+ => CountSourceShapes(compilation, returnType) >= 1;
+
+ ///
+ /// Whether a source's return type offers more than one candidate shape, which BenchmarkDotNet cannot read
+ /// unambiguously - both an enumerable and an async enumerable, or several instantiations of either one
+ /// (IEnumerable<int> plus IEnumerable<string> , say).
+ ///
+ public static bool IsAmbiguouslyEnumerable(Compilation compilation, ITypeSymbol returnType)
+ => CountSourceShapes(compilation, returnType) > 1;
+
+ ///
+ /// The element type a source declares, when its shape is unambiguous - the T of the single
+ /// IEnumerable<T> or IAsyncEnumerable<T> it offers. That is what the generated
+ /// extraction call returns, and so what any generated index is applied to.
+ ///
+ public static bool TryGetSourceElementType(Compilation compilation, ITypeSymbol returnType, out ITypeSymbol? elementType)
+ {
+ elementType = null;
+ if (CountSourceShapes(compilation, returnType) != 1)
+ {
+ return false;
+ }
+
+ var enumerable = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T);
+ var asyncEnumerable = compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1");
+
+ foreach (var candidate in new[] { returnType }.Concat(returnType.AllInterfaces))
+ {
+ if (candidate is INamedTypeSymbol { IsGenericType: true } named
+ && (SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, enumerable)
+ || SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, asyncEnumerable)))
+ {
+ elementType = named.TypeArguments[0];
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Type inference needs a *unique* candidate interface, so anything other than exactly one instantiation across
+ // both shapes fails to compile in the generated code (CS0411) - even when one element type converts to the
+ // other, as with IEnumerable plus IEnumerable. Counting keeps the two rules disjoint: none
+ // reports "unsupported shape", several reports "ambiguous shape".
+ private static int CountSourceShapes(Compilation compilation, ITypeSymbol returnType)
+ => CountInstantiations(returnType, compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T))
+ + CountInstantiations(returnType, compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"));
+
+ ///
+ /// Counts the distinct closed instantiations of that
+ /// either is or implements.
+ ///
+ private static int CountInstantiations(ITypeSymbol type, INamedTypeSymbol? interfaceDefinition)
+ {
+ if (interfaceDefinition == null)
+ {
+ return 0;
+ }
+
+ var found = new HashSet(SymbolEqualityComparer.Default);
+ if (type is INamedTypeSymbol named && SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, interfaceDefinition))
+ {
+ found.Add(named);
+ }
+ foreach (var implemented in type.AllInterfaces)
+ {
+ if (SymbolEqualityComparer.Default.Equals(implemented.OriginalDefinition, interfaceDefinition))
+ {
+ found.Add(implemented);
+ }
+ }
+ return found.Count;
+ }
+
///
/// Returns true when exposes a public parameterless GetAwaiter method —
/// the necessary precondition for the C# compiler's await binding. The analyzer doesn't drill
diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs
index aade2e2c84..52621c5769 100644
--- a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
@@ -43,12 +44,27 @@ public class ArgumentsAttributeAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_RequiresParameters_Description)));
+ internal static readonly DiagnosticDescriptor ArgumentsSourceMustReturnEnumerableRule = new(
+ DiagnosticIds.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable,
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Title)),
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Description)));
+
public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[]
{
RequiresBenchmarkAttributeRule,
MustHaveMatchingValueCountRule,
MustHaveMatchingValueTypeRule,
RequiresParametersRule,
+ ArgumentsSourceMustReturnEnumerableRule,
+ AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule,
+ AnalyzerHelper.SourceMethodMustNotBeGenericRule,
+ AnalyzerHelper.SourceElementMustNotBeByRefLikeRule,
+ AnalyzerHelper.SourceElementMayBeByRefLikeRule,
+ AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule,
}.ToImmutableArray();
public override void Initialize(AnalysisContext analysisContext)
@@ -90,15 +106,15 @@ private static void AnalyzeMethodSymbol(SymbolAnalysisContext context)
var argumentsSourceAttributes = new List();
foreach (var attr in methodSymbol.GetAttributes())
{
- if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, benchmarkAttributeTypeSymbol))
+ if (AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, benchmarkAttributeTypeSymbol))
{
hasBenchmarkAttribute = true;
}
- else if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsAttributeTypeSymbol))
+ else if (AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, argumentsAttributeTypeSymbol))
{
argumentsAttributes.Add(attr);
}
- else if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsSourceAttributeTypeSymbol))
+ else if (AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, argumentsSourceAttributeTypeSymbol))
{
argumentsSourceAttributes.Add(attr);
}
@@ -129,6 +145,14 @@ private static void AnalyzeMethodSymbol(SymbolAnalysisContext context)
foreach (var attr in argumentsAttributes)
{
+ // Only [Arguments] itself is guaranteed to carry the values in its own constructor arguments. A derived
+ // attribute declares whatever constructor it likes and may hand values to base(...), where they are
+ // invisible here, so its arguments are not the values to inspect.
+ if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsAttributeTypeSymbol))
+ {
+ continue;
+ }
+
// [Arguments]
if (attr.ConstructorArguments.Length == 0)
{
@@ -173,6 +197,117 @@ private static void AnalyzeMethodSymbol(SymbolAnalysisContext context)
}
}
+ foreach (var attr in argumentsSourceAttributes)
+ {
+ AnalyzeArgumentsSourceReturnType(attr);
+ }
+
+ void AnalyzeArgumentsSourceReturnType(AttributeData attr)
+ {
+ // These rules need the source's name, which is in this usage's own arguments only when
+ // [ArgumentsSource] itself was applied - a derived attribute may hand it to base(...), out of sight.
+ if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsSourceAttributeTypeSymbol))
+ {
+ return;
+ }
+
+ // [ArgumentsSource(nameof(Source))] or [ArgumentsSource(typeof(Other), nameof(Other.Source))]
+ ITypeSymbol? sourceType;
+ string? sourceName;
+ if (attr.ConstructorArguments.Length == 1)
+ {
+ sourceType = methodSymbol.ContainingType;
+ sourceName = attr.ConstructorArguments[0].Value as string;
+ }
+ else if (attr.ConstructorArguments.Length == 2)
+ {
+ sourceType = attr.ConstructorArguments[0].Value as ITypeSymbol;
+ sourceName = attr.ConstructorArguments[1].Value as string;
+ }
+ else
+ {
+ return;
+ }
+
+ if (sourceType == null || string.IsNullOrEmpty(sourceName))
+ {
+ return;
+ }
+
+ var referencedMember = AnalyzerHelper.FindSourceMember(sourceType, sourceName!);
+
+ if (AnalyzerHelper.SourceResolvesOnlyToGenericMethod(sourceType, sourceName!))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.SourceMethodMustNotBeGenericRule,
+ attr.GetSourceNameLocation(),
+ sourceName));
+ return;
+ }
+
+ if (AnalyzerHelper.SourceResolvesOnlyToRequiredParameterMethod(sourceType, sourceName!))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule,
+ attr.GetSourceNameLocation(),
+ sourceName));
+ return;
+ }
+
+ ITypeSymbol? returnType = referencedMember switch
+ {
+ IMethodSymbol method => method.ReturnType,
+ IPropertySymbol property => property.Type,
+ _ => null
+ };
+
+ if (returnType == null || returnType.TypeKind == TypeKind.Error)
+ {
+ return;
+ }
+
+ if (AsyncTypeShapes.IsAmbiguouslyEnumerable(context.Compilation, returnType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule,
+ attr.GetSourceNameLocation(),
+ sourceName,
+ returnType.ToDisplayString()));
+ return;
+ }
+
+ if (!AsyncTypeShapes.IsSupportedSourceReturnType(context.Compilation, returnType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ArgumentsSourceMustReturnEnumerableRule,
+ attr.GetSourceNameLocation(),
+ sourceName,
+ returnType.ToDisplayString()));
+ return;
+ }
+
+ if (!AsyncTypeShapes.TryGetSourceElementType(context.Compilation, returnType, out var elementType))
+ {
+ return;
+ }
+
+ // Discovery reads the values into an object[], which a ref struct cannot enter. Expressible since .NET 10
+ // gave IEnumerable an allows-ref-struct type parameter; a ref struct *parameter* is still supported,
+ // fed from whatever the value is built from. A constraint admitting one is answered the same way, as an
+ // open declaration is judged on what every substitution guarantees.
+ if (AnalyzerHelper.MayBeRefLike(elementType!))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.ByRefLikeRule(elementType!),
+ attr.GetSourceNameLocation(),
+ sourceName,
+ elementType!.ToDisplayString(),
+ AnalyzerHelper.ByRefLikeClause(elementType!)));
+ return;
+ }
+
+ }
+
void ReportMustHaveMatchingValueCountDiagnostic(Location diagnosticLocation, int valueCount)
=> context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueCountRule,
diagnosticLocation,
diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs
index 0bee087118..baa117e26d 100644
--- a/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs
@@ -97,7 +97,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type;
if (attributeSyntaxTypeSymbol == null
|| attributeSyntaxTypeSymbol.TypeKind == TypeKind.Error
- || !SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkCancellationAttributeTypeSymbol))
+ || !AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkCancellationAttributeTypeSymbol))
{
return;
}
diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs
index a0cf864918..ec98ae752d 100644
--- a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs
@@ -1,3 +1,4 @@
+using BenchmarkDotNet.Code;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -71,15 +72,6 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description)));
- internal static readonly DiagnosticDescriptor PropertyCannotBeInitOnlyRule = new(
- DiagnosticIds.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly,
- AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title)),
- AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat)),
- "Usage",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true,
- description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description)));
-
internal static readonly DiagnosticDescriptor ParamsSourceCannotUseWriteOnlyPropertyRule = new(
DiagnosticIds.Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty,
AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title)),
@@ -89,6 +81,24 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Description)));
+ internal static readonly DiagnosticDescriptor ParamsSourceMustReturnEnumerableRule = new(
+ DiagnosticIds.Attributes_ParamsSourceAttribute_MustReturnEnumerable,
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_MustReturnEnumerable_Title)),
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_MustReturnEnumerable_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_MustReturnEnumerable_Description)));
+
+ internal static readonly DiagnosticDescriptor ReservedMemberNameRule = new(
+ DiagnosticIds.General_ReservedMemberName,
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_ReservedMemberName_Title)),
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_ReservedMemberName_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_ReservedMemberName_Description)));
+
public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[]
{
MutuallyExclusiveOnFieldRule,
@@ -97,9 +107,15 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer
PropertyMustBePublic,
NotValidOnReadonlyFieldRule,
NotValidOnConstantFieldRule,
- PropertyCannotBeInitOnlyRule,
PropertyMustHavePublicSetterRule,
ParamsSourceCannotUseWriteOnlyPropertyRule,
+ ParamsSourceMustReturnEnumerableRule,
+ ReservedMemberNameRule,
+ AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule,
+ AnalyzerHelper.SourceMethodMustNotBeGenericRule,
+ AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule,
+ AnalyzerHelper.SourceElementMustNotBeByRefLikeRule,
+ AnalyzerHelper.SourceElementMayBeByRefLikeRule,
}.ToImmutableArray();
public override void Initialize(AnalysisContext analysisContext)
@@ -136,9 +152,9 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
if (attributeSyntaxTypeSymbol == null
|| attributeSyntaxTypeSymbol.TypeKind == TypeKind.Error
||
- (!SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsAttributeTypeSymbol)
- && !SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsSourceAttributeTypeSymbol)
- && !SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol)))
+ (!AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsAttributeTypeSymbol)
+ && !AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsSourceAttributeTypeSymbol)
+ && !AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol)))
{
return;
}
@@ -154,9 +170,12 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
Location? fieldConstModifierLocation = null;
Location? fieldReadonlyModifierLocation = null;
string fieldOrPropertyIdentifier;
- Location? propertyInitAccessorKeywordLocation = null;
Location fieldOrPropertyIdentifierLocation;
+ // One field declaration can declare several names - `[Params] public int a, b;` applies the attribute to
+ // both - and each is a member the runnable's object initializer has to bind.
+ ImmutableArray<(string Name, Location Location)> declaredNames;
bool propertyIsMissingAssignableSetter = false;
+ bool fieldOrPropertyIsStatic;
DiagnosticDescriptor fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule;
DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule;
@@ -164,6 +183,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
declaredAttributes = fieldDeclarationSyntax.AttributeLists.SelectMany(als => als.Attributes).ToImmutableArray();
fieldOrPropertyIsPublic = fieldDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword);
+ fieldOrPropertyIsStatic = fieldDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword);
var fieldConstModifierIndex = fieldDeclarationSyntax.Modifiers.IndexOf(SyntaxKind.ConstKeyword);
fieldConstModifierLocation = fieldConstModifierIndex >= 0 ? fieldDeclarationSyntax.Modifiers[fieldConstModifierIndex].GetLocation() : null;
@@ -173,6 +193,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
fieldOrPropertyIdentifier = fieldDeclarationSyntax.Declaration.Variables[0].Identifier.ToString();
fieldOrPropertyIdentifierLocation = fieldDeclarationSyntax.Declaration.Variables[0].Identifier.GetLocation();
+ declaredNames = ImmutableArray.CreateRange(fieldDeclarationSyntax.Declaration.Variables
+ .Select(variable => (variable.Identifier.ToString(), variable.Identifier.GetLocation())));
fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule = MutuallyExclusiveOnFieldRule;
fieldOrPropertyMustBePublicDiagnosticRule = FieldMustBePublic;
}
@@ -180,17 +202,19 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
declaredAttributes = propertyDeclarationSyntax.AttributeLists.SelectMany(als => als.Attributes).ToImmutableArray();
fieldOrPropertyIsPublic = propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword);
+ fieldOrPropertyIsStatic = propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword);
fieldOrPropertyIdentifier = propertyDeclarationSyntax.Identifier.ToString();
+ // `init` counts: the runnable assigns parameters through an object initializer.
+ var propertyAccessors = propertyDeclarationSyntax.AccessorList?.Accessors;
+ propertyIsMissingAssignableSetter = !HasAssignableAccessor(propertyAccessors, SyntaxKind.SetAccessorDeclaration)
#if CODE_ANALYSIS_3_8
- var propertyInitAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.InitAccessorDeclaration);
- propertyInitAccessorKeywordLocation = propertyInitAccessorIndex >= 0 ? propertyDeclarationSyntax.AccessorList!.Accessors[propertyInitAccessorIndex.Value].Keyword.GetLocation() : null;
+ && !HasAssignableAccessor(propertyAccessors, SyntaxKind.InitAccessorDeclaration)
#endif
-
- var propertySetAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.SetAccessorDeclaration);
- propertyIsMissingAssignableSetter = !propertySetAccessorIndex.HasValue || propertySetAccessorIndex.Value < 0 || propertyDeclarationSyntax.AccessorList!.Accessors[propertySetAccessorIndex.Value].Modifiers.Any();
+ ;
fieldOrPropertyIdentifierLocation = propertyDeclarationSyntax.Identifier.GetLocation();
+ declaredNames = ImmutableArray.Create((fieldOrPropertyIdentifier, fieldOrPropertyIdentifierLocation));
fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule = MutuallyExclusiveOnPropertyRule;
fieldOrPropertyMustBePublicDiagnosticRule = PropertyMustBePublic;
}
@@ -209,9 +233,10 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
fieldConstModifierLocation,
fieldReadonlyModifierLocation,
fieldOrPropertyIdentifier,
- propertyInitAccessorKeywordLocation,
propertyIsMissingAssignableSetter,
+ fieldOrPropertyIsStatic,
fieldOrPropertyIdentifierLocation,
+ declaredNames,
fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule,
fieldOrPropertyMustBePublicDiagnosticRule,
attributeSyntax,
@@ -228,21 +253,24 @@ private static void AnalyzeFieldOrPropertySymbol(
Location? fieldConstModifierLocation,
Location? fieldReadonlyModifierLocation,
string fieldOrPropertyIdentifier,
- Location? propertyInitAccessorKeywordLocation,
bool propertyIsMissingAssignableSetter,
+ bool fieldOrPropertyIsStatic,
Location fieldOrPropertyIdentifierLocation,
+ ImmutableArray<(string Name, Location Location)> declaredNames,
DiagnosticDescriptor fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule,
DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule,
AttributeSyntax attributeSyntax,
SyntaxNode attributeTarget)
{
- ImmutableArray applicableParameterAttributeTypeSymbols =
- [
+ ImmutableArray applicableParameterAttributeTypeSymbols = ImmutableArray.Create(
paramsAttributeTypeSymbol,
paramsSourceAttributeTypeSymbol,
- paramsAllValuesAttributeTypeSymbol
- ];
+ paramsAllValuesAttributeTypeSymbol);
+ // Counted by the attribute *class* written on the member, not by the parameter attribute it maps onto: a
+ // derived attribute maps to the base it derives from, so two different classes deriving from the same one
+ // are still two usages, matching how the runtime resolves them.
+ var declaredParameterAttributeTypeSymbols = new HashSet(SymbolEqualityComparer.Default);
var parameterAttributeTypeSymbols = new HashSet(SymbolEqualityComparer.Default);
foreach (var declaredAttributeSyntax in declaredAttributes)
@@ -252,23 +280,33 @@ private static void AnalyzeFieldOrPropertySymbol(
{
foreach (var applicableParameterAttributeTypeSymbol in applicableParameterAttributeTypeSymbols)
{
- if (SymbolEqualityComparer.Default.Equals(declaredAttributeTypeSymbol, applicableParameterAttributeTypeSymbol))
+ if (AnalyzerHelper.IsOrDerivesFrom(declaredAttributeTypeSymbol, applicableParameterAttributeTypeSymbol))
{
- if (!parameterAttributeTypeSymbols.Add(applicableParameterAttributeTypeSymbol))
+ // Only the very same class applied twice is CS0579's to report, and repeating it here
+ // would say it twice. Two different classes in one family are not CS0579 - the compiler
+ // is silent about them - so they must reach the duplicate rule below instead of ending
+ // the analysis and taking every other diagnostic for this member with them.
+ if (!declaredParameterAttributeTypeSymbols.Add(declaredAttributeTypeSymbol))
{
return;
}
+
+ parameterAttributeTypeSymbols.Add(applicableParameterAttributeTypeSymbol);
+
+ // At most one family per attribute; the three are siblings today, and breaking keeps the
+ // count right if one ever derives from another.
+ break;
}
}
}
}
- if (parameterAttributeTypeSymbols.Count == 0)
+ if (declaredParameterAttributeTypeSymbols.Count == 0)
{
return;
}
- if (parameterAttributeTypeSymbols.Count != 1)
+ if (declaredParameterAttributeTypeSymbols.Count != 1)
{
context.ReportDiagnostic(Diagnostic.Create(fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule,
attributeSyntax.GetLocation(),
@@ -278,6 +316,27 @@ private static void AnalyzeFieldOrPropertySymbol(
return;
}
+ // The runnable derives from the benchmark type and assigns each instance parameter member through an object
+ // initializer, which binds the member name unqualified. A parameter member named like a generated member (all
+ // __-prefixed) therefore binds to the generated member and fails to compile. (Static parameters, sources,
+ // arguments, and non-parameter members are reached via type-qualification/`base`/hiding and don't collide, so
+ // only instance parameter members are checked.)
+ // Every declared name, not only the first: the runtime reports each parameter member it cannot assign.
+ if (!fieldOrPropertyIsStatic)
+ {
+ foreach (var (name, location) in declaredNames)
+ {
+ if (RunnableConstants.ReservedInstanceMemberNames.Contains(name))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(ReservedMemberNameRule,
+ location,
+ name,
+ attributeSyntax.Name.ToString())
+ );
+ }
+ }
+ }
+
if (fieldConstModifierLocation != null)
{
context.ReportDiagnostic(Diagnostic.Create(NotValidOnConstantFieldRule,
@@ -306,15 +365,7 @@ private static void AnalyzeFieldOrPropertySymbol(
);
}
- if (propertyInitAccessorKeywordLocation != null)
- {
- context.ReportDiagnostic(Diagnostic.Create(PropertyCannotBeInitOnlyRule,
- propertyInitAccessorKeywordLocation,
- fieldOrPropertyIdentifier,
- attributeSyntax.Name.ToString())
- );
- }
- else if (propertyIsMissingAssignableSetter)
+ if (propertyIsMissingAssignableSetter)
{
context.ReportDiagnostic(Diagnostic.Create(PropertyMustHavePublicSetterRule,
fieldOrPropertyIdentifierLocation,
@@ -354,6 +405,15 @@ private static void AnalyzeParamsSourceWriteOnlyProperty(
return;
}
+ // These rules need the source's name, and the name is only in this usage's own constructor arguments when
+ // [ParamsSource] itself was applied. A derived attribute may hand it to base(...), where the runtime still
+ // reads it off the Name property but nothing here can see it - guessing from whatever arguments the derived
+ // constructor happens to take would resolve some other member, or none.
+ if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsSourceAttribute")))
+ {
+ return;
+ }
+
string? sourceName = null;
ITypeSymbol? targetType = null;
@@ -404,30 +464,89 @@ private static void AnalyzeParamsSourceWriteOnlyProperty(
return;
}
- var referencedMember = targetType.GetMembers(sourceName!).FirstOrDefault();
+ var referencedMember = AnalyzerHelper.FindSourceMember(targetType, sourceName!);
+
+ Location location = attributeData.GetSourceNameLocation();
+
if (referencedMember is IPropertySymbol propertySymbol
&& propertySymbol.SetMethod != null
&& propertySymbol.GetMethod == null)
{
- Location? location = null;
- if (attributeSyntax.ArgumentList != null)
- {
- if (attributeData.ConstructorArguments.Length == 1 && attributeSyntax.ArgumentList.Arguments.Count > 0)
- {
- location = attributeSyntax.ArgumentList.Arguments[0].Expression.GetLocation();
- }
- else if (attributeData.ConstructorArguments.Length == 2 && attributeSyntax.ArgumentList.Arguments.Count > 1)
- {
- location = attributeSyntax.ArgumentList.Arguments[1].Expression.GetLocation();
- }
- }
- location ??= attributeSyntax.GetLocation();
-
context.ReportDiagnostic(Diagnostic.Create(
ParamsSourceCannotUseWriteOnlyPropertyRule,
location,
sourceName));
+ return;
+ }
+
+ if (AnalyzerHelper.SourceResolvesOnlyToGenericMethod(targetType, sourceName!))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.SourceMethodMustNotBeGenericRule,
+ location,
+ sourceName));
+ return;
+ }
+
+ if (AnalyzerHelper.SourceResolvesOnlyToRequiredParameterMethod(targetType, sourceName!))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule,
+ location,
+ sourceName));
+ return;
+ }
+
+ ITypeSymbol? returnType = referencedMember switch
+ {
+ IMethodSymbol method => method.ReturnType,
+ IPropertySymbol property => property.Type,
+ _ => null
+ };
+
+ if (returnType == null || returnType.TypeKind == TypeKind.Error)
+ {
+ return;
+ }
+
+ if (AsyncTypeShapes.IsAmbiguouslyEnumerable(context.Compilation, returnType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule,
+ location,
+ sourceName,
+ returnType.ToDisplayString()));
+ }
+ else if (!AsyncTypeShapes.IsSupportedSourceReturnType(context.Compilation, returnType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ParamsSourceMustReturnEnumerableRule,
+ location,
+ sourceName,
+ returnType.ToDisplayString()));
+ }
+ else if (AsyncTypeShapes.TryGetSourceElementType(context.Compilation, returnType, out var elementType)
+ && AnalyzerHelper.MayBeRefLike(elementType!))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ AnalyzerHelper.ByRefLikeRule(elementType!),
+ location,
+ sourceName,
+ elementType!.ToDisplayString(),
+ AnalyzerHelper.ByRefLikeClause(elementType!)));
+ }
+ }
+
+ // An accessor that an object initializer can assign through: `set` or `init`, without an accessibility
+ // modifier (a non-public one can't be reached from the generated runnable).
+ private static bool HasAssignableAccessor(SyntaxList? accessors, SyntaxKind kind)
+ {
+ if (accessors is not { } list)
+ {
+ return false;
}
+ int index = list.IndexOf(kind);
+ return index >= 0 && !list[index].Modifiers.Any();
}
private static string? ExtractNameFromExpression(ExpressionSyntax expression, SemanticModel semanticModel)
diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs
index 7c6ed1edd7..d20e54d6b7 100644
--- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs
@@ -59,7 +59,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
var paramsAllValuesAttributeTypeSymbol = GetParamsAllValuesAttributeTypeSymbol(context.Compilation);
var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type;
- if (!SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol))
+ if (!AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol))
{
return;
}
diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs
index b1de24f56c..4d252f4f5c 100644
--- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs
@@ -73,7 +73,7 @@ private void Analyze(SymbolAnalysisContext context)
var paramsAttributeTypeSymbol = GetParamsAttributeTypeSymbol(context.Compilation);
var attrs = context.Symbol.GetAttributes();
- var paramsAttributes = attrs.Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, paramsAttributeTypeSymbol)).ToImmutableArray();
+ var paramsAttributes = attrs.Where(attr => AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, paramsAttributeTypeSymbol)).ToImmutableArray();
if (paramsAttributes.Length != 1)
{
// Don't analyze zero or multiple [Params] (multiple is not legal and already handled by GeneralParameterAttributesAnalyzer).
@@ -82,6 +82,14 @@ private void Analyze(SymbolAnalysisContext context)
var attr = paramsAttributes[0];
+ // Only [Params] itself is guaranteed to carry the values in its own constructor arguments. A derived
+ // attribute declares whatever constructor it likes and may hand values to base(...) - `BoolParams() :
+ // base(true, false)` - where they are invisible here, so its arguments are not the values to inspect.
+ if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, paramsAttributeTypeSymbol))
+ {
+ return;
+ }
+
// [Params]
if (attr.ConstructorArguments.Length == 0)
{
diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs
index 08102e7201..8303c2bf7e 100644
--- a/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs
@@ -91,7 +91,7 @@ private static void AnalyzeMethod(
{
foreach (var candidate in captured.AttributeSymbols)
{
- if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, candidate))
+ if (AnalyzerHelper.IsOrDerivesFrom(attributeData.AttributeClass, candidate))
{
matchedAttribute = candidate;
break;
diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj
index 66d475e342..40beb5a5d8 100644
--- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj
+++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj
@@ -1,40 +1,28 @@
- netstandard2.0
- false
BenchmarkDotNet.Analyzers
- true
- $(NoWarn);CS1591
-
- 5.0
- bin\$(Configuration)\roslyn$(MccVersion)\cs
- false
- $(DefineConstants);CODE_ANALYSIS_3_8
- $(DefineConstants);CODE_ANALYSIS_4_8
- $(DefineConstants);CODE_ANALYSIS_5_0
+
+
+
$(NoWarn);RS2007
$(NoWarn);RS2002
- enable
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
True
@@ -52,4 +40,4 @@
-
+
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs
index 71187fd2e5..ab49aab7c7 100644
--- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs
+++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs
@@ -316,34 +316,7 @@ internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyF
}
}
- ///
- /// Looks up a localized string similar to A property annotated with a parameter attribute must have a public, assignable setter i.e. { set; }.
- ///
- internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description {
- get {
- return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Property '{0}' annotated with [{1}] cannot be init-only.
- ///
- internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat {
- get {
- return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Properties annotated with a parameter attribute cannot have an init-only setter.
- ///
- internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title {
- get {
- return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title", resourceCulture);
- }
- }
-
- ///
+///
/// Looks up a localized string similar to A property annotated with a parameter attribute must be public.
///
internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description {
@@ -535,7 +508,187 @@ internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProper
return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title", resourceCulture);
}
}
+
+ internal static string Attributes_ParamsSourceAttribute_MustReturnEnumerable_Title {
+ get {
+ return ResourceManager.GetString("Attributes_ParamsSourceAttribute_MustReturnEnumerable_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_ReservedMemberName_Title {
+ get {
+ return ResourceManager.GetString("General_ReservedMemberName_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_ReservedMemberName_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_ReservedMemberName_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_ReservedMemberName_Description {
+ get {
+ return ResourceManager.GetString("General_ReservedMemberName_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_BenchmarkClass_RequiredMemberCannotBeSet_Title {
+ get {
+ return ResourceManager.GetString("General_BenchmarkClass_RequiredMemberCannotBeSet_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_BenchmarkClass_RequiredMemberCannotBeSet_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_BenchmarkClass_RequiredMemberCannotBeSet_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_BenchmarkClass_RequiredMemberCannotBeSet_Description {
+ get {
+ return ResourceManager.GetString("General_BenchmarkClass_RequiredMemberCannotBeSet_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Title {
+ get {
+ return ResourceManager.GetString("General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Description {
+ get {
+ return ResourceManager.GetString("General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_AmbiguousEnumerableShape_Title {
+ get {
+ return ResourceManager.GetString("General_Source_AmbiguousEnumerableShape_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_AmbiguousEnumerableShape_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_Source_AmbiguousEnumerableShape_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_AmbiguousEnumerableShape_Description {
+ get {
+ return ResourceManager.GetString("General_Source_AmbiguousEnumerableShape_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_ElementMayBeByRefLike_Title {
+ get {
+ return ResourceManager.GetString("General_Source_ElementMayBeByRefLike_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_ElementMayBeByRefLike_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_Source_ElementMayBeByRefLike_MessageFormat", resourceCulture);
+ }
+ }
+ internal static string General_Source_ElementMayBeByRefLike_Description {
+ get {
+ return ResourceManager.GetString("General_Source_ElementMayBeByRefLike_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_ElementMustNotBeByRefLike_Title {
+ get {
+ return ResourceManager.GetString("General_Source_ElementMustNotBeByRefLike_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_ElementMustNotBeByRefLike_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_Source_ElementMustNotBeByRefLike_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_ElementMustNotBeByRefLike_Description {
+ get {
+ return ResourceManager.GetString("General_Source_ElementMustNotBeByRefLike_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_MethodMustNotBeGeneric_Title {
+ get {
+ return ResourceManager.GetString("General_Source_MethodMustNotBeGeneric_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_MethodMustNotBeGeneric_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_Source_MethodMustNotBeGeneric_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_MethodMustNotBeGeneric_Description {
+ get {
+ return ResourceManager.GetString("General_Source_MethodMustNotBeGeneric_Description", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_MethodMustNotHaveRequiredParameters_Title {
+ get {
+ return ResourceManager.GetString("General_Source_MethodMustNotHaveRequiredParameters_Title", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_MethodMustNotHaveRequiredParameters_MessageFormat {
+ get {
+ return ResourceManager.GetString("General_Source_MethodMustNotHaveRequiredParameters_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string General_Source_MethodMustNotHaveRequiredParameters_Description {
+ get {
+ return ResourceManager.GetString("General_Source_MethodMustNotHaveRequiredParameters_Description", resourceCulture);
+ }
+ }
+
+ internal static string Attributes_ParamsSourceAttribute_MustReturnEnumerable_MessageFormat {
+ get {
+ return ResourceManager.GetString("Attributes_ParamsSourceAttribute_MustReturnEnumerable_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string Attributes_ParamsSourceAttribute_MustReturnEnumerable_Description {
+ get {
+ return ResourceManager.GetString("Attributes_ParamsSourceAttribute_MustReturnEnumerable_Description", resourceCulture);
+ }
+ }
+
+ internal static string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Title {
+ get {
+ return ResourceManager.GetString("Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Title", resourceCulture);
+ }
+ }
+
+ internal static string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_MessageFormat {
+ get {
+ return ResourceManager.GetString("Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_MessageFormat", resourceCulture);
+ }
+ }
+
+ internal static string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Description {
+ get {
+ return ResourceManager.GetString("Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Description", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute.
///
diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx
index e057c10de8..84a7253d78 100644
--- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx
+++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx
@@ -234,9 +234,6 @@
The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated.
-
- A property annotated with a parameter attribute must have a public, assignable setter i.e. { set; }
-
Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time
@@ -285,8 +282,95 @@
ParamsSource references a write-only property (a property with only a setter and no getter). Write-only properties cannot be read at runtime and will cause a NullReferenceException. Use a property with a getter or a method instead.
-
- Property '{0}' annotated with [{1}] cannot be init-only
+
+ ParamsSource must return a generic enumerable or async enumerable
+
+
+ ParamsSource '{0}' must return IEnumerable<T> or IAsyncEnumerable<T>, but returns '{1}'.
+
+
+ The member referenced by ParamsSource must return IEnumerable<T> or IAsyncEnumerable<T>. The non-generic IEnumerable is not enough on its own: BenchmarkDotNet's generated code infers the element type from the source, so the return type has to name it. The await-foreach pattern without the IAsyncEnumerable<T> interface, and other return types such as Task<IEnumerable<T>>, are not supported either.
+
+
+ Parameter member name is reserved by code generation
+
+
+ '{0}' is a reserved name used by BenchmarkDotNet's code generation and cannot be used for a member with {1}. Please, rename the member.
+
+
+ BenchmarkDotNet generates a runnable type that derives from the benchmark type and assigns each [Params]/[ParamsSource]/[ParamsAllValues] member through an object initializer. A parameter member whose name matches one of the generated members (e.g. __GlobalSetup, __WorkloadActionUnroll) binds to the generated member instead and makes the generated program fail to compile. Rename the member.
+
+
+ Required member cannot be set by BenchmarkDotNet
+
+
+ '{0}' is a required member that BenchmarkDotNet cannot set. Only [Params]/[ParamsSource]/[ParamsAllValues] and [BenchmarkCancellation] members are set when the benchmark is constructed. Please, remove the 'required' modifier or apply one of those attributes.
+
+
+ BenchmarkDotNet constructs the benchmark type and can only set required members at construction time: [Params]/[ParamsSource]/[ParamsAllValues] members and an instance [BenchmarkCancellation] member. A required member set later (e.g. in [GlobalSetup]) or not set at all makes the generated program fail to compile. Remove the 'required' modifier or apply one of the supported attributes.
+
+
+ Benchmark constructor must not be annotated with [SetsRequiredMembers]
+
+
+ The constructor of '{0}' is annotated with [SetsRequiredMembers], which BenchmarkDotNet's generated constructor cannot honor. Please, remove the attribute and set each required member with [Params]/[ParamsSource]/[ParamsAllValues] or [BenchmarkCancellation].
+
+
+ BenchmarkDotNet generates a runnable type deriving from the benchmark type, whose constructor chains to the benchmark type's constructor. C# requires that constructor to repeat [SetsRequiredMembers] (CS9039), which would suppress all required-member checking and silently hide required members BenchmarkDotNet cannot set. Remove the attribute and set each required member with [Params]/[ParamsSource]/[ParamsAllValues] or [BenchmarkCancellation].
+
+
+ Source must not have more than one enumerable shape
+
+
+ The source '{0}' returns '{1}', which has more than one enumerable shape, so BenchmarkDotNet cannot tell which one to read the values from. Please, return a single IEnumerable<T> or IAsyncEnumerable<T>.
+
+
+ A [ParamsSource]/[ArgumentsSource] member whose return type offers several candidate shapes is ambiguous - both IEnumerable<T> and IAsyncEnumerable<T>, or several instantiations of either one, such as IEnumerable<int> together with IEnumerable<string>. BenchmarkDotNet's generated code infers the element type from the source, and type inference requires a unique candidate, so the generated code fails to compile. This holds even when one element type converts to the other. Return a single enumerable or async enumerable.
+
+
+ A source method must not be generic
+
+
+ Source '{0}' resolves only to a generic method, which BenchmarkDotNet cannot invoke. Please, make the method non-generic, or add a non-generic overload taking no required parameters.
+
+
+ BenchmarkDotNet invokes a source method directly, and nothing supplies a generic method's type arguments - there is no call site to infer them from. Invoking one fails inside reflection with a message that says nothing about the benchmark, so the declaration is reported instead. A non-generic overload whose parameters are all optional, or a property with a public getter, serves as the source where one exists.
+
+
+ A source must not yield a ref struct
+
+
+ Source '{0}' is declared to yield '{1}', which {2}. BenchmarkDotNet reads a source's values through reflection, which cannot carry one. Please, yield a type the values can be built from - an array for a Span parameter - and let the benchmark take the ref struct.
+
+
+ BenchmarkDotNet reads a source's values into an object[] during discovery, so every value has to survive being boxed, and a ref struct cannot be. Since .NET 10 the framework's IEnumerable<T> allows a ref struct type argument, so such a source compiles and then fails inside reflection with a message that says nothing about the benchmark. A source yielding a type parameter constrained with 'allows ref struct' is reported the same way: an open declaration is judged on what every type argument guarantees, and that constraint guarantees nothing about boxing. A ref struct parameter is supported and remains so: declare the source to yield what the value is built from, such as IEnumerable<byte[]> for a ReadOnlySpan<byte> parameter, which the generated code converts.
+
+
+ A source may yield a ref struct
+
+
+ Source '{0}' is declared to yield '{1}', which {2}. A type argument that is not by-ref-like reads normally; one that is fails while BenchmarkDotNet reads the values. Please, yield a type the values can be built from - an array for a Span parameter - and let the benchmark take the ref struct.
+
+
+ BenchmarkDotNet reads a source's values into an object[] during discovery, so every value has to survive being boxed, and a ref struct cannot be. A source yielding a type parameter constrained with 'allows ref struct' says nothing about whether any particular type argument is one: closed to int it reads like any other value type, and closed to a ref struct it fails while the values are read, which BenchmarkDotNet reports naming the element. The compiler cannot tell which type arguments a benchmark will be given, so this is a warning rather than an error - unlike BDN1311, which reports a ref struct it can see. A ref struct parameter is supported and remains so: declare the source to yield what the value is built from, such as IEnumerable<byte[]> for a ReadOnlySpan<byte> parameter, which the generated code converts.
+
+
+ Source method must not have required parameters
+
+
+ The source '{0}' has required parameters. A [ParamsSource]/[ArgumentsSource] source method must be parameterless or have only optional parameters. Please, make its parameters optional.
+
+
+ A method referenced by [ParamsSource] or [ArgumentsSource] is invoked by BenchmarkDotNet with no arguments (optional parameters, such as an [EnumeratorCancellation] token, receive their defaults). A source method with a required parameter cannot be invoked and is not recognized as a source. Make its parameters optional or remove them.
+
+
+ ArgumentsSource must return a generic enumerable or async enumerable
+
+
+ ArgumentsSource '{0}' must return IEnumerable<T> or IAsyncEnumerable<T>, but returns '{1}'.
+
+
+ The member referenced by ArgumentsSource must return IEnumerable<T> or IAsyncEnumerable<T>. The non-generic IEnumerable is not enough on its own: BenchmarkDotNet's generated code infers the element type from the source, so the return type has to name it. The await-foreach pattern without the IAsyncEnumerable<T> interface, and other return types such as Task<IEnumerable<T>>, are not supported either.
Duplicate parameter attribute on property '{0}'
@@ -324,9 +408,6 @@
The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type
-
- Properties annotated with a parameter attribute cannot have an init-only setter
-
Only one parameter attribute can be applied to a property
@@ -460,4 +541,4 @@ Either add the [ArgumentsSource] or [Arguments] attribute(s) or remove the param
The runtime and toolchain characteristics of a job are coupled: setting the toolchain overwrites the runtime with the one the toolchain targets, and setting the runtime clears any explicitly set toolchain. When both are set on the same job, whichever assignment comes last wins and the other is silently discarded — so the outcome depends on ordering rather than on anything visible at the call site. Prefer setting only the Toolchain, which already determines the runtime; the default fix removes the Runtime assignment. If you instead intended the Runtime to win, remove the Toolchain.
-
\ No newline at end of file
+
diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs
index 6dd467fc56..63b188769b 100644
--- a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs
@@ -202,7 +202,7 @@ bool HasBenchmarkAttribute()
{
if (attributeData.AttributeClass != null)
{
- if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, benchmarkAttributeTypeSymbol))
+ if (AnalyzerHelper.IsOrDerivesFrom(attributeData.AttributeClass, benchmarkAttributeTypeSymbol))
{
return true;
}
diff --git a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs
index 497d939b0d..9ab09701b8 100644
--- a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs
+++ b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs
@@ -15,25 +15,34 @@ public static class DiagnosticIds
public const string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed = "BDN1106";
public const string General_BenchmarkClass_OnlyOneMethodCanBeBaseline = "BDN1107";
public const string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory = "BDN1108";
+ public const string General_BenchmarkClass_RequiredMemberCannotBeSet = "BDN1109";
+ public const string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers = "BDN1110";
public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField = "BDN1200";
public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty = "BDN1201";
public const string Attributes_GeneralParameterAttributes_FieldMustBePublic = "BDN1202";
public const string Attributes_GeneralParameterAttributes_PropertyMustBePublic = "BDN1203";
public const string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField = "BDN1204";
public const string Attributes_GeneralParameterAttributes_NotValidOnConstantField = "BDN1205";
- public const string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly = "BDN1206";
public const string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter = "BDN1207";
+ public const string General_ReservedMemberName = "BDN1208";
public const string Attributes_ParamsAttribute_MustHaveValues = "BDN1300";
public const string Attributes_ParamsAttribute_MustHaveMatchingValueType = "BDN1301";
public const string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute = "BDN1302";
public const string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType = "BDN1303";
public const string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool = "BDN1304";
public const string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty = "BDN1305";
+ public const string Attributes_ParamsSourceAttribute_MustReturnEnumerable = "BDN1306";
+ public const string General_Source_MethodMustNotHaveRequiredParameters = "BDN1307";
+ public const string General_Source_AmbiguousEnumerableShape = "BDN1308";
+ public const string General_Source_MethodMustNotBeGeneric = "BDN1310";
+ public const string General_Source_ElementMustNotBeByRefLike = "BDN1311";
+ public const string General_Source_ElementMayBeByRefLike = "BDN1312";
public const string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters = "BDN1400";
public const string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute = "BDN1500";
public const string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount = "BDN1501";
public const string Attributes_ArgumentsAttribute_MustHaveMatchingValueType = "BDN1502";
public const string Attributes_ArgumentsAttribute_RequiresParameters = "BDN1503";
+ public const string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable = "BDN1504";
public const string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType = "BDN1600";
public const string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic = "BDN1601";
public const string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic = "BDN1602";
diff --git a/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs
index ad4af424f8..ba825dfb67 100644
--- a/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs
@@ -75,7 +75,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
{
var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type;
if (attributeSyntaxTypeSymbol != null &&
- SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol))
+ AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol))
{
hasBenchmarkAttribute = true;
break;
@@ -112,7 +112,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
{
foreach (var attribute in member.GetAttributes())
{
- if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkCancellationAttributeTypeSymbol))
+ if (AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkCancellationAttributeTypeSymbol))
{
hasCancellationTokenMember = true;
break;
diff --git a/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs
index 8686d6615b..83f711fd36 100644
--- a/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs
@@ -92,7 +92,7 @@ private static void AnalyzeMethod(
{
foreach (var candidate in captured.AttributeSymbols)
{
- if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, candidate))
+ if (AnalyzerHelper.IsOrDerivesFrom(attributeData.AttributeClass, candidate))
{
matchedAttribute = candidate;
break;
diff --git a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs
index 84f5be7e4b..20c1c6206b 100644
--- a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs
+++ b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs
@@ -131,7 +131,8 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
}
}
- var genericTypeArgumentsAttributes = AnalyzerHelper.GetAttributes("BenchmarkDotNet.Attributes.GenericTypeArgumentsAttribute", context.Compilation, classDeclarationSyntax.AttributeLists, context.SemanticModel);
+ var genericTypeArgumentsAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.GenericTypeArgumentsAttribute");
+ var genericTypeArgumentsAttributes = AnalyzerHelper.GetAttributes(genericTypeArgumentsAttributeTypeSymbol, classDeclarationSyntax.AttributeLists, context.SemanticModel);
if (genericTypeArgumentsAttributes.Length > 0)
{
foreach (var genericTypeArgumentsAttribute in genericTypeArgumentsAttributes)
@@ -140,7 +141,11 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
{
context.ReportDiagnostic(Diagnostic.Create(ClassWithGenericTypeArgumentsAttributeMustBeGenericRule, genericTypeArgumentsAttribute.GetLocation()));
}
- else if (genericTypeArgumentsAttribute.ArgumentList is { Arguments.Count: > 0 })
+ // Only [GenericTypeArguments] itself is guaranteed to carry the type arguments in its own constructor
+ // arguments. A derived attribute declares whatever constructor it likes and may hand them to base(...),
+ // where they are invisible here, so its arguments are not the ones to count.
+ else if (genericTypeArgumentsAttribute.ArgumentList is { Arguments.Count: > 0 }
+ && SymbolEqualityComparer.Default.Equals(context.SemanticModel.GetTypeInfo(genericTypeArgumentsAttribute).Type, genericTypeArgumentsAttributeTypeSymbol))
{
if (genericTypeArgumentsAttribute.ArgumentList.Arguments.Count != classDeclarationSyntax.TypeParameterList.Parameters.Count)
{
@@ -190,12 +195,22 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
continue;
}
- if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol))
+ if (AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol))
{
benchmarkAttributeUsages.Add(attributeSyntax);
}
- else if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkCategoryAttributeTypeSymbol))
+ else if (AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkCategoryAttributeTypeSymbol))
{
+ // Only [BenchmarkCategory] itself carries the categories in its own arguments; a derived
+ // attribute may hand them to base(...), where they are invisible. Harvesting its own
+ // arguments would report the wrong category set rather than an unknown one.
+ if (!SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkCategoryAttributeTypeSymbol))
+ {
+ hasBenchmarkCategoryCompilerDiagnostics = true;
+
+ continue;
+ }
+
if (attributeSyntax.ArgumentList is { Arguments.Count: 1 })
{
// Check if this is an explicit params array creation
@@ -435,12 +450,23 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
foreach (var attribute in methodAttributes)
{
- if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkAttributeTypeSymbol))
+ if (AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkAttributeTypeSymbol))
{
benchmarkAttributeUsages.Add(attribute);
}
- else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkCategoryAttributeTypeSymbol))
+ else if (AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkCategoryAttributeTypeSymbol))
{
+ // Only [BenchmarkCategory] itself is guaranteed to carry the categories in its own
+ // constructor arguments; a derived attribute may hand them to base(...), where they
+ // are invisible here. The categories are then unknown rather than absent, so treat
+ // the method as unanalyzable instead of as uncategorized.
+ if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkCategoryAttributeTypeSymbol))
+ {
+ hasBenchmarkCategoryCompilerDiagnostics = true;
+
+ break;
+ }
+
foreach (var benchmarkCategoriesArray in attribute.ConstructorArguments)
{
if (!benchmarkCategoriesArray.IsNull)
@@ -532,6 +558,8 @@ private static void AnalyzeAttributeSyntax(SyntaxNodeAnalysisContext context)
var benchmarkCategoryAttributeTypeSymbol = GetBenchmarkCategoryAttributeTypeSymbol(context.Compilation);
var attributeTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type;
+ // Only [BenchmarkCategory] itself: this rule is about a null *category*, and a derived attribute's single
+ // argument is whatever its own constructor takes - reporting on it flags an argument that is not a category.
if (SymbolEqualityComparer.Default.Equals(attributeTypeSymbol, benchmarkCategoryAttributeTypeSymbol))
{
if (attributeSyntax.ArgumentList is { Arguments.Count: 1 })
diff --git a/src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs b/src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs
new file mode 100644
index 0000000000..21f1911e42
--- /dev/null
+++ b/src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs
@@ -0,0 +1,41 @@
+#if !CODE_ANALYSIS_3_0
+using System.Reflection;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Microsoft.CodeAnalysis;
+
+// ITypeSymbol.IsRefLikeType only appears in Roslyn 3.0 (dotnet/roslyn#30426), though the compiler has tracked
+// ref-likeness since C# 7.2 - as the internal TypeSymbol.IsByRefLikeType this reads. Reflection rather than the
+// metadata attribute because that route is deliberately closed: PENamedTypeSymbol.GetAttributes filters
+// IsByRefLikeAttribute out exactly when the type is ref-like, so a referenced Span reports no such attribute.
+// The property is declared on the base and overridden per symbol kind, so one lookup serves every kind.
+//
+// Both guards below answer false rather than throwing, because an exception escaping an analyzer is reported as
+// AD0001 and disables it. The lookup is guarded because a failure in a static initializer poisons the type for
+// every later call; the instance check is guarded because GetValue is the one call here that can throw, and does
+// so only for an ITypeSymbol that is not a Roslyn C# symbol - which the language filter on every analyzer here
+// makes unreachable, RS1009 forbids implementing, and a runtime mock could still produce.
+internal static class RefLikeTypePolyfill
+{
+ private static readonly PropertyInfo? IsByRefLikeType = ResolveIsByRefLikeType();
+
+ internal static bool IsRefLikeType(ITypeSymbol type)
+ => IsByRefLikeType is { } property
+ && property.DeclaringType!.IsInstanceOfType(type)
+ && property.GetValue(type) is true;
+
+ private static PropertyInfo? ResolveIsByRefLikeType()
+ {
+ try
+ {
+ return typeof(CSharpCompilation).Assembly
+ .GetType("Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol")
+ ?.GetProperty("IsByRefLikeType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
+#endif
diff --git a/src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs b/src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs
new file mode 100644
index 0000000000..ac350dc5fb
--- /dev/null
+++ b/src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs
@@ -0,0 +1,19 @@
+#if !CODE_ANALYSIS_3_8
+using System.Collections.Generic;
+
+namespace Microsoft.CodeAnalysis;
+
+// SymbolEqualityComparer appears in Roslyn 3.3, so the 2.8 and 3.0 bands compare symbols through ISymbol.Equals,
+// as callers did before it existed. Declared in Roslyn's own namespace, so the call sites read the same on
+// every band.
+internal sealed class SymbolEqualityComparer : IEqualityComparer
+{
+ internal static readonly SymbolEqualityComparer Default = new();
+
+ private SymbolEqualityComparer() { }
+
+ public bool Equals(ISymbol? x, ISymbol? y) => x is null ? y is null : x.Equals(y);
+
+ public int GetHashCode(ISymbol obj) => obj.GetHashCode();
+}
+#endif
diff --git a/src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs
new file mode 100644
index 0000000000..72b34d16a8
--- /dev/null
+++ b/src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs
@@ -0,0 +1,200 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace BenchmarkDotNet.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class RequiredMemberAnalyzer : DiagnosticAnalyzer
+{
+ internal static readonly DiagnosticDescriptor RequiredMemberCannotBeSetRule = new(
+ DiagnosticIds.General_BenchmarkClass_RequiredMemberCannotBeSet,
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_RequiredMemberCannotBeSet_Title)),
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_RequiredMemberCannotBeSet_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_RequiredMemberCannotBeSet_Description)));
+
+ internal static readonly DiagnosticDescriptor ConstructorMustNotSetRequiredMembersRule = new(
+ DiagnosticIds.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers,
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Title)),
+ AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_MessageFormat)),
+ "Usage",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Description)));
+
+ public override ImmutableArray SupportedDiagnostics
+ => ImmutableArray.Create(RequiredMemberCannotBeSetRule, ConstructorMustNotSetRequiredMembersRule);
+
+ // Attributes whose members BDN sets when constructing the benchmark (via the object initializer or the
+ // cancellation-token initializer). A `required` member with any of these is satisfied at construction.
+ private static readonly string[] SettableMemberAttributeNames =
+ [
+ "BenchmarkDotNet.Attributes.ParamsAttribute",
+ "BenchmarkDotNet.Attributes.ParamsSourceAttribute",
+ "BenchmarkDotNet.Attributes.ParamsAllValuesAttribute",
+ "BenchmarkDotNet.Attributes.BenchmarkCancellationAttribute",
+ ];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+#if CODE_ANALYSIS_4_4
+ // `required` members (and the IPropertySymbol/IFieldSymbol.IsRequired API) are C# 11 / Roslyn 4.4+.
+ context.RegisterCompilationStartAction(startContext =>
+ {
+ var benchmarkAttribute = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(startContext.Compilation);
+ if (benchmarkAttribute == null)
+ {
+ return;
+ }
+
+ var settableAttributes = SettableMemberAttributeNames
+ .Select(startContext.Compilation.GetTypeByMetadataName)
+ .Where(symbol => symbol != null)
+ .ToImmutableArray();
+ var setsRequiredMembersAttribute = startContext.Compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute");
+
+ // The runnable derives from the benchmark type, so it must set every `required` member the type declares
+ // OR inherits. Each benchmark type checks its own base chain, stopping at a base that is itself a
+ // benchmark type (that one reports its own members), so no state is shared between types.
+ startContext.RegisterSymbolAction(symbolContext =>
+ {
+ var benchmarkType = (INamedTypeSymbol)symbolContext.Symbol;
+ if (benchmarkType.TypeKind != TypeKind.Class || !IsBenchmarkType(benchmarkType, benchmarkAttribute))
+ {
+ return;
+ }
+
+ // The generated constructor chains to this one, so C# would force it to repeat [SetsRequiredMembers]
+ // (CS9039) - which suppresses required-member checking entirely and hides required members BDN
+ // cannot set. Report it rather than propagating the attribute into the generated code.
+ if (setsRequiredMembersAttribute != null)
+ {
+ foreach (var constructor in benchmarkType.InstanceConstructors)
+ {
+ if (constructor.Parameters.Length != 0
+ || !constructor.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, setsRequiredMembersAttribute)))
+ {
+ continue;
+ }
+
+ var constructorLocation = constructor.Locations.FirstOrDefault(candidate => candidate.IsInSource);
+ if (constructorLocation != null)
+ {
+ symbolContext.ReportDiagnostic(Diagnostic.Create(ConstructorMustNotSetRequiredMembersRule, constructorLocation, benchmarkType.Name));
+ }
+ }
+ }
+
+ Location? inheritedLocation = null;
+
+ for (INamedTypeSymbol? current = benchmarkType; current != null && current.SpecialType != SpecialType.System_Object; current = current.BaseType)
+ {
+ bool declaredOnBenchmarkType = SymbolEqualityComparer.Default.Equals(current, benchmarkType);
+
+ // A base that is itself a benchmark type reports its own (and its bases') required members at
+ // their declarations via its own walk, so stop here to avoid flagging them again. That walk only
+ // happens for a base declared in this compilation - one from a referenced assembly is never
+ // analyzed, so keep going and report its members at this type's base-type reference instead.
+ if (!declaredOnBenchmarkType
+ && IsBenchmarkType(current, benchmarkAttribute)
+ && current.DeclaringSyntaxReferences.Length > 0)
+ {
+ break;
+ }
+
+ foreach (var member in current.GetMembers())
+ {
+ bool isRequired = member switch
+ {
+ IPropertySymbol property => property.IsRequired,
+ IFieldSymbol field => field.IsRequired,
+ _ => false
+ };
+ if (!isRequired)
+ {
+ continue;
+ }
+
+ // BDN sets [Params*] members via the object initializer and an instance [BenchmarkCancellation]
+ // member via the cancellation-token initializer, so those required members are satisfied.
+ if (member.GetAttributes().Any(attribute => settableAttributes.Any(settable => AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, settable))))
+ {
+ continue;
+ }
+
+ // A member declared on the benchmark type is reported at its own declaration; one inherited
+ // from a base (source or a referenced assembly) is reported at the benchmark class's
+ // `: BaseType` reference - the base declaration doesn't know it's inherited by a benchmark,
+ // and that reference is always in the benchmark class's own source.
+ Location location = declaredOnBenchmarkType
+ ? member.Locations.FirstOrDefault(candidate => candidate.IsInSource) ?? Location.None
+ : inheritedLocation ??= GetBaseTypeReferenceLocation(benchmarkType);
+
+ symbolContext.ReportDiagnostic(Diagnostic.Create(RequiredMemberCannotBeSetRule, location, member.Name));
+ }
+ }
+ }, SymbolKind.NamedType);
+ });
+#endif
+ }
+
+ private static bool IsBenchmarkType(INamedTypeSymbol type, INamedTypeSymbol benchmarkAttribute)
+ {
+ // The [Benchmark] method may be inherited, so walk the base types too. The attribute itself may also be
+ // a user's own deriving from [Benchmark], which is what the runtime resolves.
+ for (INamedTypeSymbol? current = type; current != null; current = current.BaseType)
+ {
+ if (current.GetMembers().OfType()
+ .Any(method => method.GetAttributes().Any(attribute => AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkAttribute))))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // The `: BaseType` reference in the benchmark class's declaration (its base class in the base list), where an
+ // inherited required member becomes the benchmark class's problem. Matched by name to avoid a semantic-model
+ // lookup (RS1030); the base class is always the first base-list entry, but a name match is robust to partials
+ // and interface-only base lists. Falls back to the type's own declaration.
+ private static Location GetBaseTypeReferenceLocation(INamedTypeSymbol benchmarkType)
+ {
+ var baseType = benchmarkType.BaseType;
+ if (baseType != null && baseType.SpecialType != SpecialType.System_Object)
+ {
+ foreach (var syntaxReference in benchmarkType.DeclaringSyntaxReferences)
+ {
+ if (syntaxReference.GetSyntax() is TypeDeclarationSyntax typeDeclaration && typeDeclaration.BaseList != null)
+ {
+ foreach (var baseTypeSyntax in typeDeclaration.BaseList.Types)
+ {
+ if (GetRightmostName(baseTypeSyntax.Type) == baseType.Name)
+ {
+ return baseTypeSyntax.GetLocation();
+ }
+ }
+ }
+ }
+ }
+
+ return benchmarkType.Locations.FirstOrDefault(candidate => candidate.IsInSource) ?? Location.None;
+ }
+
+ private static string? GetRightmostName(TypeSyntax type) => type switch
+ {
+ IdentifierNameSyntax identifier => identifier.Identifier.ValueText,
+ GenericNameSyntax generic => generic.Identifier.ValueText,
+ QualifiedNameSyntax qualified => GetRightmostName(qualified.Right),
+ AliasQualifiedNameSyntax alias => GetRightmostName(alias.Name),
+ _ => null
+ };
+}
diff --git a/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj b/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj
index 2117f52ecf..557201c372 100644
--- a/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj
+++ b/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj
@@ -1,31 +1,14 @@
-
+
- netstandard2.0
- false
BenchmarkDotNet.CodeFixers
- true
- $(NoWarn);CS1591
- 5.0
- bin\$(Configuration)\roslyn$(MccVersion)\cs
- false
- $(DefineConstants);CODE_ANALYSIS_3_8
- $(DefineConstants);CODE_ANALYSIS_4_8
- $(DefineConstants);CODE_ANALYSIS_5_0
- enable
+
-
-
-
-
-
-
-
-
+
-
+
\ No newline at end of file
diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
index 7d641b2357..e98f17201b 100644
--- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs
@@ -54,8 +54,16 @@ public EtwProfiler(EtwProfilerConfig config)
public RunMode GetRunMode(BenchmarkCase benchmarkCase) => runMode;
- public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters)
- => HardwareCounters.Validate(validationParameters, mandatory: false).ToAsyncEnumerable();
+ // Iterated here rather than through BenchmarkDotNet's ToAsyncEnumerable polyfill: that one is internal to
+ // BenchmarkDotNet and compiled out of its .NET 10 asset, while this assembly is netstandard2.0 and would
+ // bind the netstandard asset's copy - which a .NET 10 host then cannot load.
+ public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters)
+ {
+ foreach (var error in HardwareCounters.Validate(validationParameters, mandatory: false))
+ {
+ yield return error;
+ }
+ }
public ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parameters, CancellationToken cancellationToken)
{
diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj
index 07c28cb008..014d70872c 100644
--- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj
+++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj
@@ -1,4 +1,4 @@
-
+
@@ -32,7 +32,6 @@
-
diff --git a/src/BenchmarkDotNet/Code/ArrayParam.cs b/src/BenchmarkDotNet/Code/ArrayParam.cs
deleted file mode 100644
index 548547f195..0000000000
--- a/src/BenchmarkDotNet/Code/ArrayParam.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using BenchmarkDotNet.Extensions;
-using BenchmarkDotNet.Helpers;
-using JetBrains.Annotations;
-using System.Reflection;
-
-namespace BenchmarkDotNet.Code
-{
- internal static class ArrayParam
- {
- private static (string BaseElementTypeRepr, string InnerDimensions) GetDisplayString(Type arrayType)
- {
- var elemType = arrayType.GetElementType()!;
-
- if (elemType.IsArray)
- {
- var (baseElementTypeRepr, innerDimensions) = GetDisplayString(elemType);
-
- return (baseElementTypeRepr, $"[{new string(',', arrayType.GetArrayRank() - 1)}]{innerDimensions}");
- }
-
- return (elemType.GetDisplayName(), $"[{new string(',', arrayType.GetArrayRank() - 1)}]");
- }
-
- public static string GetDisplayString(Array array)
- {
- string dimensionRepr = string.Join(", ", Enumerable.Range(0, array.Rank).Select(array.GetLength));
-
- var (baseElementTypeRepr, innerDimensions) = GetDisplayString(array.GetType());
-
- innerDimensions = string.Join("", innerDimensions.Split([']'], count: 2).Skip(1));
-
- return $"{baseElementTypeRepr}[{dimensionRepr}]{innerDimensions}";
- }
- }
-
- public class ArrayParam : IParam
- {
- private readonly T[] array;
- private readonly Func? toSourceCode;
-
- private ArrayParam(T[] array, Func? toSourceCode = null)
- {
- this.array = array;
- this.toSourceCode = toSourceCode;
- }
-
- public object Value => array;
-
- public string DisplayText => ArrayParam.GetDisplayString(array);
-
- public string ToSourceCode()
- => $"new {typeof(T).GetCorrectCSharpTypeName()}[] {{ {string.Join(", ", array.Select(item => toSourceCode?.Invoke(item) ?? SourceCodeHelper.ToSourceCode(item)))} }}";
-
- ///
- /// for types where calling .ToString() will be enough to re-create them in auto-generated source code file (integers, strings and other primitives)
- ///
- public static ArrayParam ForPrimitives(T[] array) => new ArrayParam(array);
-
- ///
- /// for types where calling .ToString() will be NOT enough to re-create them in auto-generated source code file
- ///
- /// the array
- /// method which transforms an item of type T to it's C# representation
- /// example: point => $"new Point2d({point.X}, {point.Y})"
- ///
- [PublicAPI] public static ArrayParam ForComplexTypes(T[] array, Func toSourceCode) => new ArrayParam(array, toSourceCode);
-
- internal static IParam? FromObject(object array)
- {
- var type = array.GetType();
- if (!type.IsArray)
- throw new InvalidOperationException("The argument must be an array");
- var elementType = type.GetElementType();
- if (elementType == null)
- throw new InvalidOperationException("Failed to determine type of array elements");
- if (!SourceCodeHelper.IsCompilationTimeConstant(elementType))
- throw new InvalidOperationException("The argument must be an array of primitives");
-
- var arrayParamType = typeof(ArrayParam<>).MakeGenericType(elementType);
-
- var methodInfo = arrayParamType.GetMethod(nameof(ForPrimitives), BindingFlags.Public | BindingFlags.Static)
- ?? throw new InvalidOperationException($"{nameof(ForPrimitives)} not found");
- return (IParam?)methodInfo.Invoke(null, [array]);
- }
- }
-}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs b/src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs
new file mode 100644
index 0000000000..3d667171a4
--- /dev/null
+++ b/src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs
@@ -0,0 +1,180 @@
+using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
+using BenchmarkDotNet.Parameters;
+using BenchmarkDotNet.Running;
+using System.Reflection;
+
+namespace BenchmarkDotNet.Code;
+
+///
+/// Renders descriptions as C# expressions for the generated runnable.
+/// All generated syntax lives here, so the parameters themselves stay language-neutral.
+///
+internal sealed class CSharpParameterRenderer
+{
+ // The CancellationToken local declared at the top of the generated Run method.
+ private const string CancellationTokenLocalName = "cancellationToken";
+
+ // One entry per SourceRead reached through an instance member, in assignment order. The member is invoked in
+ // the constructor - the only place `base` is reachable - and the sequence passed out to Run, which extracts
+ // from it. Keyed by the read rather than by the parameter, so an arguments row invokes the member once; two
+ // [ParamsSource] members naming the same one still get a local each, because members range over their values
+ // independently and each read is its own object. Names are positional, so two sources sharing a simple name
+ // cannot collide.
+ private readonly IReadOnlyList<(SourceRead Read, string Local)> instanceReads;
+
+ // One entry per SourceRead whose values are assigned by statement rather than by the object initializer -
+ // arguments and static [ParamsSource] members. A statement block can hold a local, so the read is extracted
+ // once into one and every parameter indexes into that. The initializer cannot, so a read used only there stays
+ // an inline expression; nothing is shared between members anyway.
+ private readonly IReadOnlyList<(SourceRead Read, string Local)> statementReads;
+
+ private CSharpParameterRenderer(
+ IReadOnlyList<(SourceRead, string)> instanceReads,
+ IReadOnlyList<(SourceRead, string)> statementReads)
+ {
+ this.instanceReads = instanceReads;
+ this.statementReads = statementReads;
+ }
+
+ public static CSharpParameterRenderer Create(BenchmarkCase benchmarkCase)
+ {
+ var instanceReads = new List<(SourceRead, string)>();
+ var statementReads = new List<(SourceRead, string)>();
+
+ foreach (var parameter in benchmarkCase.Parameters.Items)
+ {
+ if (parameter.ParameterValue is not ParameterValue.FromSource fromSource)
+ continue;
+
+ if (!IsStatic(fromSource.Read.Source))
+ Reserve(instanceReads, fromSource.Read, "source");
+
+ // Everything the object initializer does not assign is assigned by a statement, which can hold a local.
+ if (parameter.IsArgument || parameter.IsStatic)
+ Reserve(statementReads, fromSource.Read, "read");
+ }
+
+ return new CSharpParameterRenderer(instanceReads, statementReads);
+
+ static void Reserve(List<(SourceRead, string)> reads, SourceRead read, string prefix)
+ {
+ foreach (var reserved in reads)
+ {
+ if (ReferenceEquals(reserved.Item1, read))
+ return;
+ }
+
+ reads.Add((read, prefix + reads.Count));
+ }
+ }
+
+ /// `out IEnumerable<T> source0, ...` - the ctor's parameter list, and the `new` call's argument list.
+ /// `out T x` parses both as a parameter declaration and as an out-variable declaration expression,
+ /// so the same text serves the declaration and the call site.
+ public string RenderSourceOutParameters()
+ => string.Join(", ", instanceReads.Select(read =>
+ $"out {ReturnType(read.Read.Source).GetCorrectCSharpTypeName()} {read.Local}"));
+
+ /// `source0 = base.Values();` - the ctor body statements that read the instance sources.
+ public string RenderSourceCaptures()
+ => string.Join(
+ Environment.NewLine,
+ instanceReads.Select(read => $" {read.Local} = base.{read.Read.Source.Name}{InvocationPostfix(read.Read.Source)};"));
+
+ /// `Element read0 = ...;` - one extraction per read, ahead of the statements that index into it.
+ public IEnumerable RenderStatementReads()
+ => statementReads.Select(read => $"{ElementTypeName(read.Read)} {read.Local} = {Extraction(read.Read)};");
+
+ public string Render(ParameterValue value)
+ {
+ switch (value)
+ {
+ case ParameterValue.Constant constant:
+ return SourceCodeHelper.ToSourceCode(constant.Value, constant.Type);
+
+ case ParameterValue.FromSource fromSource:
+ {
+ // The value can't be embedded, so the child process re-obtains it by enumerating the source.
+ // GetParameterAsync returns the source's element type, so an element index binds directly.
+ string cast = $"({fromSource.TargetType.GetCorrectCSharpTypeName()})";
+ string elementIndex = fromSource.ElementIndex is { } index ? $"[{index}]" : string.Empty;
+
+ string source = StatementLocal(fromSource.Read) is { } local ? local : $"({Extraction(fromSource.Read)})";
+
+ return $"{cast}{source}{elementIndex}";
+ }
+
+ default:
+ throw new NotSupportedException($"{value.GetType().Name} is not a supported {nameof(ParameterValue)}.");
+ }
+ }
+
+ // The generated code declares its locals with explicit, fully qualified types, as the template does.
+ private static string ElementTypeName(SourceRead read)
+ {
+ TryGetElementTypeName(read, out string name);
+
+ return name;
+ }
+
+ // False where the source does not name an element type. The extraction then has no type argument to bind
+ // against and does not compile, which is reported before any of this is emitted - `var` only keeps the
+ // declaration from adding a second error on top of that one.
+ private static bool TryGetElementTypeName(SourceRead read, out string name)
+ {
+ if (read.Source.GetSourceReturnType().TryGetSourceElementType(out var elementType))
+ {
+ name = elementType.GetCorrectCSharpTypeName();
+ return true;
+ }
+
+ name = "var";
+ return false;
+ }
+
+ // Fully qualified (#778, #1007, #2821).
+ private string Extraction(SourceRead read)
+ {
+ string typeArgument = TryGetElementTypeName(read, out string elementTypeName)
+ ? $"<{elementTypeName}>"
+ : string.Empty;
+
+ return $"await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(global::BenchmarkDotNet.Parameters.ParameterExtractor.GetParameterAsync{typeArgument}({SourceExpression(read)}, {read.ValueIndex}, {CancellationTokenLocalName}))";
+ }
+
+ private string? StatementLocal(SourceRead read)
+ {
+ foreach (var reserved in statementReads)
+ {
+ if (ReferenceEquals(reserved.Read, read))
+ return reserved.Local;
+ }
+
+ return null;
+ }
+
+ // A static source is called where the value is assigned; an instance source is read into a local in the
+ // constructor, because `base` is not reachable from Run's object initializer.
+ private string SourceExpression(SourceRead read)
+ {
+ if (IsStatic(read.Source))
+ return $"{read.Source.DeclaringType!.GetCorrectCSharpTypeName()}.{read.Source.Name}{InvocationPostfix(read.Source)}";
+
+ foreach (var reserved in instanceReads)
+ {
+ if (ReferenceEquals(reserved.Read, read))
+ return reserved.Local;
+ }
+
+ throw new InvalidOperationException($"No local was reserved for the value {read.Source.Name} provides.");
+ }
+
+ private static string InvocationPostfix(MemberInfo source) => source is PropertyInfo ? string.Empty : "()";
+
+ private static bool IsStatic(MemberInfo source)
+ => source is PropertyInfo property ? property.GetMethod!.IsStatic : ((MethodInfo) source).IsStatic;
+
+ private static Type ReturnType(MemberInfo source)
+ => source is PropertyInfo property ? property.GetMethod!.ReturnType : ((MethodInfo) source).ReturnType;
+}
diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs
index c575d01713..708495ab32 100644
--- a/src/BenchmarkDotNet/Code/CodeGenerator.cs
+++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs
@@ -1,6 +1,5 @@
using BenchmarkDotNet.Characteristics;
using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Disassemblers;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
@@ -29,19 +28,24 @@ internal static async ValueTask GenerateAsync(BuildPartition buildPartit
var declarationsProvider = GetDeclarationsProvider(benchmark);
var extraFields = declarationsProvider.GetExtraFields();
+ var parameterRenderer = CSharpParameterRenderer.Create(benchmark);
string benchmarkTypeCode = declarationsProvider
.ReplaceTemplate(new SmartStringBuilder(benchmarkTypeTemplate))
.Replace("$ID$", buildInfo.Id.ToString())
.Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark))
- .Replace("$ParamsContent$", GetParamsContent(benchmark))
+ .Replace("$SourceOutParameters$", parameterRenderer.RenderSourceOutParameters())
+ .Replace("$SourceOutArguments$", parameterRenderer.RenderSourceOutParameters())
+ .Replace("$SourceCaptures$", parameterRenderer.RenderSourceCaptures())
+ .Replace("$ParamsInitializer$", GetParamsInitializer(benchmark, parameterRenderer))
.Replace("$CancellationTokenAssignment$", GetCancellationTokenAssignment(benchmark))
+ .Replace("$CancellationTokenInitializer$", GetCancellationTokenInitializer(benchmark))
.Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark))
.Replace("$DeclareFieldsContainer$", GetDeclareFieldsContainer(benchmark, buildInfo.Id, extraFields))
- .Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark))
+ .Replace("$StaticParamsAndArgsContent$", GetStaticParamsAndArgsContent(benchmark, parameterRenderer))
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
.Replace("$RunExtraIteration$", buildInfo.Config.HasExtraIterationDiagnoser(benchmark) ? "true" : "false")
- .Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
+ .Replace("$DisassemblerEntryMethodName$", RunnableConstants.ForDisassemblyDiagnoserMethodName)
.Replace("$InProcessDiagnoserRouters$", GetInProcessDiagnoserRouters(buildInfo))
.ToString();
@@ -137,46 +141,96 @@ private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchm
}
// internal for tests
+ internal static string GetParamsInitializer(BenchmarkCase benchmarkCase)
+ => GetParamsInitializer(benchmarkCase, CSharpParameterRenderer.Create(benchmarkCase));
- internal static string GetParamsContent(BenchmarkCase benchmarkCase)
+ private static string GetParamsInitializer(BenchmarkCase benchmarkCase, CSharpParameterRenderer renderer)
=> string.Join(
- string.Empty,
+ $",{Environment.NewLine} ",
benchmarkCase.Parameters.Items
- .Where(parameter => !parameter.IsArgument)
- .Select(parameter => $"{(parameter.IsStatic ? benchmarkCase.Descriptor.Type.GetCorrectCSharpTypeName() : "base")}.{parameter.Name} = {parameter.ToSourceCode()};"));
+ .Where(parameter => !parameter.IsArgument && !parameter.IsStatic)
+ .Select(parameter => $"{parameter.Name} = {renderer.Render(parameter.ParameterValue)}"));
+ // Static [BenchmarkCancellation] members only - instance members are set by the object initializer
+ // (GetCancellationTokenInitializer), so emitting them here too would assign them twice.
internal static string GetCancellationTokenAssignment(BenchmarkCase benchmarkCase)
{
var targetType = benchmarkCase.Descriptor.Type;
- var cancellationTokenMembers = new System.Collections.Generic.List();
+ List cancellationTokenMembers = [];
var typeFullName = targetType.GetCorrectCSharpTypeName();
+ // As in GetCancellationTokenInitializer: one entry per name. Here a repeat compiles, since these are
+ // statements, but `Type.Name` binds to the most derived member both times - so it would assign that
+ // one twice and the member hiding it never.
+ HashSet emitted = new(StringComparer.Ordinal);
+
+ // FlattenHierarchy reaches a base type's statics, which reflection otherwise withholds - the same set
+ // BenchmarkCancellationValidator reports on, so it cannot accept a member this never assigns.
// Check properties
- foreach (var property in targetType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static))
+ foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
- if (property.PropertyType == typeof(System.Threading.CancellationToken) &&
+ if (property.PropertyType == typeof(CancellationToken) &&
property.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) &&
property.CanWrite &&
- property.GetSetMethod() is { } setter)
+ property.GetSetMethod() is { IsStatic: true } &&
+ emitted.Add(property.Name))
{
- var target = setter.IsStatic ? typeFullName : "base";
- cancellationTokenMembers.Add($" {target}.{property.Name} = cancellationToken;");
+ cancellationTokenMembers.Add($" {typeFullName}.{property.Name} = cancellationToken;");
}
}
// Check fields
- foreach (var field in targetType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static))
+ foreach (var field in targetType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
- if (field.FieldType == typeof(System.Threading.CancellationToken) &&
- field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false))
+ if (field.FieldType == typeof(CancellationToken) &&
+ field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) &&
+ emitted.Add(field.Name))
{
- var target = field.IsStatic ? typeFullName : "base";
- cancellationTokenMembers.Add($" {target}.{field.Name} = cancellationToken;");
+ cancellationTokenMembers.Add($" {typeFullName}.{field.Name} = cancellationToken;");
}
}
return cancellationTokenMembers.Count > 0
- ? string.Join(System.Environment.NewLine, cancellationTokenMembers) + System.Environment.NewLine
+ ? string.Join(Environment.NewLine, cancellationTokenMembers) + Environment.NewLine
+ : string.Empty;
+ }
+
+ private static string GetCancellationTokenInitializer(BenchmarkCase benchmarkCase)
+ {
+ var targetType = benchmarkCase.Descriptor.Type;
+ List entries = [];
+
+ // One entry per name. GetFields hands back a hidden base field alongside the `new` one that hides it -
+ // GetProperties does not, which is why only fields reach this - and the same name twice in an object
+ // initializer is CS1912. The name binds to the most derived member either way, so the second entry
+ // could only ever repeat the first.
+ HashSet emitted = new(StringComparer.Ordinal);
+
+ foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
+ {
+ if (property.PropertyType == typeof(CancellationToken) &&
+ property.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) &&
+ property.CanWrite &&
+ property.GetSetMethod() is { IsStatic: false } &&
+ emitted.Add(property.Name))
+ {
+ entries.Add($"{property.Name} = cancellationToken,");
+ }
+ }
+
+ foreach (var field in targetType.GetFields(BindingFlags.Public | BindingFlags.Instance))
+ {
+ if (field.FieldType == typeof(CancellationToken) &&
+ field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) &&
+ !field.IsStatic &&
+ emitted.Add(field.Name))
+ {
+ entries.Add($"{field.Name} = cancellationToken,");
+ }
+ }
+
+ return entries.Count > 0
+ ? string.Join($"{Environment.NewLine} ", entries)
: string.Empty;
}
@@ -184,12 +238,12 @@ 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()} {RunnableConstants.ArgParamPrefix}{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};")
+ .Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name!)).GetCorrectCSharpTypeName()} {RunnableConstants.ArgFieldPrefix}{index};")
.Concat(extraFields)
.ToArray();
@@ -200,9 +254,9 @@ private static string GetDeclareFieldsContainer(BenchmarkCase benchmarkCase, Ben
}
var sb = new StringBuilder();
- sb.AppendLine("""
+ sb.AppendLine($$"""
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)]
- private struct FieldsContainer
+ private struct {{RunnableConstants.FieldsContainerTypeName}}
{
""");
foreach (var field in fields)
@@ -211,28 +265,38 @@ private struct FieldsContainer
}
sb.AppendLine(" }");
sb.AppendLine();
- sb.AppendLine($" private global::BenchmarkDotNet.Autogenerated.Runnable_{benchmarkId.Value}.FieldsContainer __fieldsContainer;");
+ sb.AppendLine($" private global::{RunnableConstants.EmittedTypePrefix}{benchmarkId.Value}.{RunnableConstants.FieldsContainerTypeName} {RunnableConstants.FieldsContainerName};");
return sb.ToString();
}
/*
-
+
[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)]
- private unsafe struct FieldsContainer
+ private unsafe struct __FieldsContainer
{
$DeclareArgumentFields$
$ExtraFields$
}
- private global::BenchmarkDotNet.Autogenerated.Runnable_$ID$.FieldsContainer __fieldsContainer;
-
+ private global::BenchmarkDotNet.Autogenerated.Runnable_$ID$.__FieldsContainer __fieldsContainer;
+
*/
- private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase)
- => string.Join(
- Environment.NewLine,
- benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
- .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
+ // Assigned after the instance is created: argument fields live on it, and a static parameter may draw its
+ // value from an instance source, which the constructor captures.
+ private static string GetStaticParamsAndArgsContent(BenchmarkCase benchmarkCase, CSharpParameterRenderer renderer)
+ {
+ var staticParams = benchmarkCase.Parameters.Items
+ .Where(parameter => !parameter.IsArgument && parameter.IsStatic)
+ .Select(parameter => $"{benchmarkCase.Descriptor.Type.GetCorrectCSharpTypeName()}.{parameter.Name} = {renderer.Render(parameter.ParameterValue)};");
+
+ var argumentFields = benchmarkCase.Descriptor.WorkloadMethod.GetParameters()
+ .Select((parameter, index) => $"instance.{RunnableConstants.FieldsContainerName}.{RunnableConstants.ArgFieldPrefix}{index} = {renderer.Render(benchmarkCase.Parameters.GetArgument(parameter.Name!).ParameterValue)};");
+
+ return string.Join(
+ $"{Environment.NewLine} ",
+ renderer.RenderStatementReads().Concat(staticParams).Concat(argumentFields));
+ }
private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase)
{
@@ -407,10 +471,10 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen
if (runCallType == CodeGenBenchmarkRunCallType.Reflection)
{
// Use reflection to call benchmark's Run method indirectly.
- return """
+ return $$"""
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)
+ .GetType($"{{RunnableConstants.EmittedTypePrefix}}{id}")
+ .GetMethod("{{RunnableConstants.RunMethodName}}", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static)
.Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode }))
.ConfigureAwait(false);
""";
@@ -422,7 +486,7 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen
foreach (var buildInfo in buildPartition.Benchmarks)
{
- @switch.AppendLine($"case {buildInfo.Id.Value}: await BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;");
+ @switch.AppendLine($"case {buildInfo.Id.Value}: await {RunnableConstants.EmittedTypePrefix}{buildInfo.Id.Value}.{RunnableConstants.RunMethodName}(host, benchmarkName, diagnoserRunMode); break;");
}
@switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");");
@@ -433,9 +497,9 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen
private static Type GetFieldType(Type argumentType, ParameterInstance argument)
{
- // #774 we can't store Span in a field, so we store an array (which is later casted to Span when we load the arguments)
- if (argumentType.IsStackOnlyWithImplicitCast(argument.Value))
- return argument.Value.GetType();
+ // #774 we can't store ByRefLike in a field, so we store what the value is cast to (which is later converted back to the ByRefLike when we load the arguments).
+ if (argumentType.WithoutRefModifier().IsByRefLike() && argument.Value is { } value)
+ return value.GetType();
return argumentType;
}
diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
index 155fba85ee..03490585fe 100644
--- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
+++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs
@@ -99,7 +99,7 @@ protected string GetLoadArguments()
.Select((parameter, index) =>
{
var refModifier = parameter.ParameterType.IsByRef ? "ref" : string.Empty;
- return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.__fieldsContainer.argField{index};";
+ return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.{RunnableConstants.FieldsContainerName}.{RunnableConstants.ArgFieldPrefix}{index};";
})
);
@@ -110,7 +110,7 @@ protected string GetPassArguments()
.Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}")
);
- // Renders the benchmark method's parameter types as a Type[] for __ResolveWorkloadMethods to match overloads
+ // Renders the benchmark method's parameter types as a Type[] for Run's workload-method resolution to match overloads
// exactly. Each is a typeof(...) of the element type, re-wrapping by-ref/pointer via reflection (typeof can't
// express `T&`), so resolution never has to name the method's (possibly unspellable) return type.
private string GetWorkloadMethodParameterTypes()
@@ -134,7 +134,7 @@ protected string GetPassArgumentsDirect()
=> string.Join(
", ",
Descriptor.WorkloadMethod.GetParameters()
- .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} this.__fieldsContainer.argField{index}")
+ .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} this.{RunnableConstants.FieldsContainerName}.{RunnableConstants.ArgFieldPrefix}{index}")
);
}
@@ -148,7 +148,7 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
string passArguments = GetPassArguments();
string workloadMethodCall = GetWorkloadMethodCall(passArguments);
string coreImpl = $$"""
- private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.OverheadActionUnrollMethodName}}({{CoreParameters}})
{
unsafe
{
@@ -156,13 +156,13 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
{{StartClockSyncCode}}
while (--invokeCount >= 0)
{
- this.__Overhead({{passArguments}});@Unroll@
+ this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});@Unroll@
}
{{ReturnSyncCode}}
}
}
- private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.OverheadActionNoUnrollMethodName}}({{CoreParameters}})
{
unsafe
{
@@ -170,13 +170,13 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
{{StartClockSyncCode}}
while (--invokeCount >= 0)
{
- this.__Overhead({{passArguments}});
+ this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});
}
{{ReturnSyncCode}}
}
}
- private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.WorkloadActionUnrollMethodName}}({{CoreParameters}})
{
unsafe
{
@@ -190,7 +190,7 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
}
}
- private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.WorkloadActionNoUnrollMethodName}}({{CoreParameters}})
{
unsafe
{
@@ -223,29 +223,29 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
string passArguments = GetPassArguments();
string workloadMethodCall = $"global::{typeof(AwaitHelper).FullName}.{nameof(AwaitHelper.GetResult)}({GetWorkloadMethodCall(passArguments).TrimEnd(';')});";
string coreImpl = $$"""
- private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.OverheadActionUnrollMethodName}}({{CoreParameters}})
{
{{loadArguments}}
{{StartClockSyncCode}}
while (--invokeCount >= 0)
{
- this.__Overhead({{passArguments}});@Unroll@
+ this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});@Unroll@
}
{{ReturnSyncCode}}
}
- private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.OverheadActionNoUnrollMethodName}}({{CoreParameters}})
{
{{loadArguments}}
{{StartClockSyncCode}}
while (--invokeCount >= 0)
{
- this.__Overhead({{passArguments}});
+ this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});
}
{{ReturnSyncCode}}
}
- private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.WorkloadActionUnrollMethodName}}({{CoreParameters}})
{
{{loadArguments}}
{{StartClockSyncCode}}
@@ -256,7 +256,7 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
{{ReturnSyncCode}}
}
- private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.WorkloadActionNoUnrollMethodName}}({{CoreParameters}})
{
{{loadArguments}}
{{StartClockSyncCode}}
@@ -282,19 +282,19 @@ internal abstract class AsyncDeclarationsProviderBase(BenchmarkCase benchmark) :
public override string[] GetExtraFields() =>
[
- $"public {typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()} workloadValueTaskSource;",
- $"public {typeof(IClock).GetCorrectCSharpTypeName()} clock;",
- "public long invokeCount;"
+ $"public {typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()} {RunnableConstants.WorkloadValueTaskSourceFieldName};",
+ $"public {typeof(IClock).GetCorrectCSharpTypeName()} {RunnableConstants.ClockFieldName};",
+ $"public long {RunnableConstants.InvokeCountFieldName};"
];
protected override string GetExtraGlobalSetupImpl()
=> $$"""
- this.__fieldsContainer.workloadValueTaskSource = new {{typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()}}();
- this.__StartWorkload();
+ this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}} = new {{typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()}}();
+ this.{{RunnableConstants.StartWorkloadMethodName}}();
""";
protected override string GetExtraGlobalCleanupImpl()
- => "this.__fieldsContainer.workloadValueTaskSource.Complete();";
+ => $"this.{RunnableConstants.FieldsContainerName}.{RunnableConstants.WorkloadValueTaskSourceFieldName}.Complete();";
protected bool TryGetAsyncMethodBuilderAttribute(out string asyncMethodBuilderAttribute)
{
@@ -355,57 +355,57 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString
Type workloadCoreReturnType = GetWorkloadCoreReturnType(hasAsyncMethodBuilderAttribute, WorkloadAwaitableReturnType);
string finalReturn = GetFinalReturn(workloadCoreReturnType);
string coreImpl = $$"""
- private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.OverheadActionUnrollMethodName}}({{CoreParameters}})
{
- return this.OverheadActionNoUnroll(invokeCount * {{unrollFactor}}, clock);
+ return this.{{RunnableConstants.OverheadActionNoUnrollMethodName}}(invokeCount * {{unrollFactor}}, clock);
}
- private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.OverheadActionNoUnrollMethodName}}({{CoreParameters}})
{
{{StartClockSyncCode}}
while (--invokeCount >= 0)
{
- this.__Overhead({{passArguments}});
+ this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});
}
{{ReturnSyncCode}}
}
- private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.WorkloadActionUnrollMethodName}}({{CoreParameters}})
{
- return this.WorkloadActionNoUnroll(invokeCount * {{unrollFactor}}, clock);
+ return this.{{RunnableConstants.WorkloadActionNoUnrollMethodName}}(invokeCount * {{unrollFactor}}, clock);
}
- private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}})
+ private {{CoreReturnType}} {{RunnableConstants.WorkloadActionNoUnrollMethodName}}({{CoreParameters}})
{
- this.__fieldsContainer.invokeCount = invokeCount;
- this.__fieldsContainer.clock = clock;
+ this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.InvokeCountFieldName}} = invokeCount;
+ this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.ClockFieldName}} = clock;
// The source is allocated and the workload loop started in __GlobalSetup,
// so this hot path is branchless and allocation-free.
- return this.__fieldsContainer.workloadValueTaskSource.Continue();
+ return this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.Continue();
}
- private async void __StartWorkload()
+ private async void {{RunnableConstants.StartWorkloadMethodName}}()
{
- await __WorkloadCore();
+ await {{RunnableConstants.WorkloadCoreMethodName}}();
}
-
+
{{asyncMethodBuilderAttribute}}
- private async {{workloadCoreReturnType.GetCorrectCSharpTypeName()}} __WorkloadCore()
+ private async {{workloadCoreReturnType.GetCorrectCSharpTypeName()}} {{RunnableConstants.WorkloadCoreMethodName}}()
{
try
{
- if (await this.__fieldsContainer.workloadValueTaskSource.GetIsComplete())
+ if (await this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.GetIsComplete())
{
{{finalReturn}}
}
while (true)
{
- {{typeof(StartedClock).GetCorrectCSharpTypeName()}} startedClock = {{typeof(ClockExtensions).GetCorrectCSharpTypeName()}}.Start(this.__fieldsContainer.clock);
- while (--this.__fieldsContainer.invokeCount >= 0)
+ {{typeof(StartedClock).GetCorrectCSharpTypeName()}} startedClock = {{typeof(ClockExtensions).GetCorrectCSharpTypeName()}}.Start(this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.ClockFieldName}});
+ while (--this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.InvokeCountFieldName}} >= 0)
{
{{GetCallAndConsumeImpl(workloadMethodCall)}}
}
- if (await this.__fieldsContainer.workloadValueTaskSource.SetResultAndGetIsComplete(startedClock.GetElapsed()))
+ if (await this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.SetResultAndGetIsComplete(startedClock.GetElapsed()))
{
{{finalReturn}}
}
@@ -413,7 +413,7 @@ private async void __StartWorkload()
}
catch (global::System.Exception e)
{
- __fieldsContainer.workloadValueTaskSource.SetException(e);
+ {{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.SetException(e);
{{finalReturn}}
}
}
@@ -457,4 +457,4 @@ protected override string GetCallAndConsumeImpl(string workloadMethodCall)
""";
}
}
-}
\ No newline at end of file
+}
diff --git a/src/BenchmarkDotNet/Code/EnumParam.cs b/src/BenchmarkDotNet/Code/EnumParam.cs
deleted file mode 100644
index 976a8fd25c..0000000000
--- a/src/BenchmarkDotNet/Code/EnumParam.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using BenchmarkDotNet.Extensions;
-using System.Globalization;
-
-namespace BenchmarkDotNet.Code
-{
- public class EnumParam : IParam
- {
- // Preserves type information for enum values from F# code
- // See also:
- // https://github.com/dotnet/fsharp/issues/995
- private readonly Type type;
-
- private EnumParam(object value, Type type)
- {
- this.Value = value;
- this.type = type;
- }
-
- public object Value { get; }
-
- public string DisplayText => $"{Enum.ToObject(type, Value)}";
-
- public string ToSourceCode() =>
- $"({type.GetCorrectCSharpTypeName()})({ToInvariantCultureString()})";
-
- internal static IParam FromObject(object value, Type? type = null)
- {
- type = type ?? value.GetType();
- if (!type.IsEnum)
- throw new ArgumentOutOfRangeException(nameof(type));
-
- return new EnumParam(value, type);
- }
-
- private string ToInvariantCultureString()
- {
- switch (Type.GetTypeCode(Enum.GetUnderlyingType(type)))
- {
- case TypeCode.Byte:
- return ((byte)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.Int16:
- return ((short)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.Int32:
- return ((int)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.Int64:
- return ((long)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.SByte:
- return ((sbyte)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.UInt16:
- return ((ushort)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.UInt32:
- return ((uint)Value).ToString(CultureInfo.InvariantCulture);
- case TypeCode.UInt64:
- return ((ulong)Value).ToString(CultureInfo.InvariantCulture);
- default:
- throw new ArgumentOutOfRangeException(nameof(Value));
- }
- }
- }
-}
diff --git a/src/BenchmarkDotNet/Code/IParam.cs b/src/BenchmarkDotNet/Code/IParam.cs
deleted file mode 100644
index eb094b35bd..0000000000
--- a/src/BenchmarkDotNet/Code/IParam.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace BenchmarkDotNet.Code
-{
- public interface IParam
- {
- ///
- /// value of the parameter object for benchmark
- /// used internally (e.g. by the InProcessToolchain)
- ///
- object Value { get; }
-
- ///
- /// used to display the value (e.g. in summary in Params column)
- ///
- string DisplayText { get; }
-
- ///
- /// this source code is used to create parameter for benchmark
- /// in C# source code file
- /// example: $"new Point2D({Value.X}, {Value.Y})"
- ///
- string ToSourceCode();
- }
-}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Code/RunnableConstants.cs b/src/BenchmarkDotNet/Code/RunnableConstants.cs
new file mode 100644
index 0000000000..ea434ce6c0
--- /dev/null
+++ b/src/BenchmarkDotNet/Code/RunnableConstants.cs
@@ -0,0 +1,63 @@
+using System.Collections.Immutable;
+
+namespace BenchmarkDotNet.Code;
+
+// Member names emitted by the code generator onto the generated runnable type - used by both the out-of-process
+// template (Templates/BenchmarkType.txt via CodeGenerator/DeclarationsProvider) and the in-process IL emitter
+// (RunnableEmitter). ReservedInstanceMemberNames (below) reuses these so the reserved-name checks can't drift.
+internal class RunnableConstants
+{
+ public const string EmittedTypePrefix = "BenchmarkDotNet.Autogenerated.Runnable_";
+ public const string ArgFieldPrefix = "argField";
+ public const string ArgParamPrefix = "arg";
+
+ public const string InvokeCountParamName = "invokeCount";
+ public const string ClockParamName = "clock";
+
+ public const string RunMethodName = "__Run";
+ public const string FieldsContainerTypeName = "__FieldsContainer";
+ public const string FieldsContainerName = "__fieldsContainer";
+ public const string WorkloadValueTaskSourceFieldName = "workloadValueTaskSource";
+ public const string ClockFieldName = "clock";
+ public const string InvokeCountFieldName = "invokeCount";
+
+ public const string TrickTheJitCoreMethodName = "__TrickTheJIT__";
+ public const string ForDisassemblyDiagnoserMethodName = "__ForDisassemblyDiagnoser__";
+ public const string GlobalSetupMethodName = "__GlobalSetup";
+ public const string GlobalCleanupMethodName = "__GlobalCleanup";
+ public const string IterationSetupMethodName = "__IterationSetup";
+ public const string IterationCleanupMethodName = "__IterationCleanup";
+ public const string OverheadImplementationMethodName = "__Overhead";
+ public const string OverheadActionUnrollMethodName = "__OverheadActionUnroll";
+ public const string OverheadActionNoUnrollMethodName = "__OverheadActionNoUnroll";
+ public const string WorkloadActionUnrollMethodName = "__WorkloadActionUnroll";
+ public const string WorkloadActionNoUnrollMethodName = "__WorkloadActionNoUnroll";
+ public const string StartWorkloadMethodName = "__StartWorkload";
+ public const string WorkloadCoreMethodName = "__WorkloadCore";
+
+ // The constants above that name members the generated runnable declares. A [Params]/[ParamsSource]/
+ // [ParamsAllValues] member with one of these names is shadowed by the generated member and makes the generated
+ // program fail to compile (CS1913 in the object initializer, or an inaccessible-member error). Shared by
+ // ParamsValidator (runtime) and the reserved-name analyzer (compile time) via a linked file.
+ // NOTE: ImmutableHashSet.Create (not a collection expression) so this linked file also compiles against the
+ // older System.Collections.Immutable used by the analyzer's Roslyn targets.
+ public static readonly ImmutableHashSet ReservedInstanceMemberNames = ImmutableHashSet.Create(StringComparer.Ordinal,
+ [
+ RunMethodName,
+ FieldsContainerTypeName,
+ FieldsContainerName,
+ TrickTheJitCoreMethodName,
+ OverheadImplementationMethodName,
+ OverheadActionUnrollMethodName,
+ OverheadActionNoUnrollMethodName,
+ WorkloadActionUnrollMethodName,
+ WorkloadActionNoUnrollMethodName,
+ ForDisassemblyDiagnoserMethodName,
+ GlobalSetupMethodName,
+ GlobalCleanupMethodName,
+ IterationSetupMethodName,
+ IterationCleanupMethodName,
+ StartWorkloadMethodName,
+ WorkloadCoreMethodName,
+ ]);
+}
diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs
index f84712834d..15c3d4764e 100644
--- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs
+++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs
@@ -79,6 +79,8 @@ public IEnumerable GetValidators()
yield return DeferredExecutionValidator.FailOnError;
yield return ParamsAllValuesValidator.FailOnError;
yield return ParamsValidator.FailOnError;
+ yield return RequiredMemberValidator.FailOnError;
+ yield return SourceReturnTypeValidator.FailOnError;
yield return BenchmarkCancellationValidator.FailOnError;
}
diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
index e7208faeef..9a35e5af97 100644
--- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
+++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
@@ -30,6 +30,8 @@ public static class ImmutableConfigBuilder
DeferredExecutionValidator.DontFailOnError,
ParamsAllValuesValidator.FailOnError,
ParamsValidator.FailOnError,
+ RequiredMemberValidator.FailOnError,
+ SourceReturnTypeValidator.FailOnError,
BenchmarkCancellationValidator.FailOnError
};
diff --git a/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs b/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs
index 73d5c68414..a922903ec9 100644
--- a/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs
+++ b/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs
@@ -1,20 +1,30 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Running;
namespace BenchmarkDotNet.ConsoleArguments
{
- public class CorrectionsSuggester
+ internal sealed class CorrectionsSuggester
{
// note This is a heuristic value, we suppose that user can make three or fewer typos.
private static int PossibleTyposCount => 3;
private readonly HashSet possibleBenchmarkNameFilters = [];
private readonly HashSet actualFullBenchmarkNames = [];
- public CorrectionsSuggester(IReadOnlyList types)
+ internal CorrectionsSuggester(IReadOnlyList types)
+ => Populate(TypeFilter.Filter(DefaultConfig.Instance, types));
+
+ private CorrectionsSuggester(BenchmarkRunInfo[] benchmarkRunInfos)
+ => Populate(benchmarkRunInfos);
+
+ internal static async ValueTask CreateAsync(IReadOnlyList types, CancellationToken cancellationToken = default)
+ => new(await TypeFilter.FilterAsync(DefaultConfig.Instance, types, cancellationToken).ConfigureAwait());
+
+ private void Populate(BenchmarkRunInfo[] benchmarkRunInfos)
{
- foreach (var benchmarkRunInfo in TypeFilter.Filter(DefaultConfig.Instance, types))
+ foreach (var benchmarkRunInfo in benchmarkRunInfos)
{
foreach (var benchmarkCase in benchmarkRunInfo.BenchmarksCases)
{
diff --git a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
index 31b82a11d8..2ef7487f56 100644
--- a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
+++ b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs
@@ -11,6 +11,7 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.ComponentModel;
+using System.Runtime.CompilerServices;
namespace BenchmarkDotNet.Diagnosers
{
@@ -55,8 +56,22 @@ public void DisplayResults(ILogger logger)
}
}
+ // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why.
public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters)
- => diagnosers.ToAsyncEnumerable().SelectMany(diagnoser => diagnoser.ValidateAsync(validationParameters));
+ => ValidateAsyncCore(validationParameters);
+
+ private async IAsyncEnumerable ValidateAsyncCore(ValidationParameters validationParameters, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ foreach (var diagnoser in diagnosers)
+ {
+#pragma warning disable CA2007
+ await foreach (var error in diagnoser.ValidateAsync(validationParameters).ConfigureAwait(cancellationToken))
+#pragma warning restore CA2007
+ {
+ yield return error;
+ }
+ }
+ }
}
public sealed class CompositeInProcessDiagnoser(IReadOnlyList inProcessDiagnosers)
@@ -123,4 +138,4 @@ public async ValueTask HandleAsync(BenchmarkSignal signal, CancellationToken can
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs
index 41faf28111..f28cd207b3 100644
--- a/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs
+++ b/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs
@@ -1,3 +1,4 @@
+using BenchmarkDotNet.Code;
using System.Text.Json.Serialization;
namespace BenchmarkDotNet.Disassemblers
@@ -17,7 +18,7 @@ internal struct ClrMdArgs(int processId, string typeName, string methodName, boo
internal bool PrintSource = printSource;
[JsonInclude]
- internal int MaxDepth = methodName == DisassemblerConstants.DisassemblerEntryMethodName && maxDepth != int.MaxValue ? maxDepth + 1 : maxDepth;
+ internal int MaxDepth = methodName == RunnableConstants.ForDisassemblyDiagnoserMethodName && maxDepth != int.MaxValue ? maxDepth + 1 : maxDepth;
[JsonInclude]
internal string[] Filters = filters;
diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs
index 580fd82afb..8690168193 100644
--- a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs
+++ b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs
@@ -1,3 +1,4 @@
+using BenchmarkDotNet.Code;
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Extensions;
@@ -96,7 +97,7 @@ internal DisassemblyResult AttachAndDisassemble(ClrMdArgs args)
// we don't want to export the disassembler entry point method which is just an artificial method added to get generic types working
var filteredMethods = disassembledMethods.Length == 1
? disassembledMethods // if there is only one method we want to return it (most probably benchmark got inlined)
- : disassembledMethods.Where(method => !method.Name.Contains(DisassemblerConstants.DisassemblerEntryMethodName)).ToArray();
+ : disassembledMethods.Where(method => !method.Name.Contains(RunnableConstants.ForDisassemblyDiagnoserMethodName)).ToArray();
return new DisassemblyResult
{
diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs
index e3917ce21a..dbc76f737b 100644
--- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs
+++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs
@@ -211,11 +211,6 @@ public sealed class DisassemblyResult
public Dictionary AddressToNameMapping { get; set; } = [];
}
-public static class DisassemblerConstants
-{
- public const string DisassemblerEntryMethodName = "__ForDisassemblyDiagnoser__";
-}
-
internal sealed class State
{
internal State(ClrRuntime runtime, Version runtimeVersion)
diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
index cb4412ad08..adddee11dc 100644
--- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
+++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs
@@ -1,4 +1,5 @@
using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Code;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Detectors;
using BenchmarkDotNet.Disassemblers;
@@ -78,7 +79,7 @@ private ClrMdArgs BuildClrMdArgs(BenchmarkCase benchmarkCase, string typeName, i
=> new(
processId: processId,
typeName: typeName,
- methodName: DisassemblerConstants.DisassemblerEntryMethodName,
+ methodName: RunnableConstants.ForDisassemblyDiagnoserMethodName,
printSource: Config.PrintSource,
maxDepth: Config.MaxDepth,
filters: Config.Filters,
diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs
index 0e222adb28..b9d45340a0 100644
--- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs
+++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs
@@ -36,6 +36,15 @@ public void Dispose()
public T ExecuteUntilComplete(ValueTask valueTask)
=> context.ExecuteUntilComplete(valueTask);
+
+ public void ExecuteUntilComplete(ValueTask valueTask)
+ => context.ExecuteUntilComplete(WithResult(valueTask));
+
+ private static async ValueTask WithResult(ValueTask valueTask)
+ {
+ await valueTask.ConfigureAwait(false);
+ return true;
+ }
}
// We implement a specialized context that does not inherit from SynchronizationContext, because we never install a SynchronizationContext.Current.
diff --git a/src/BenchmarkDotNet/Exporters/FullNameProvider.cs b/src/BenchmarkDotNet/Exporters/FullNameProvider.cs
index 19555908d2..c8d32a20e0 100644
--- a/src/BenchmarkDotNet/Exporters/FullNameProvider.cs
+++ b/src/BenchmarkDotNet/Exporters/FullNameProvider.cs
@@ -134,8 +134,6 @@ private static string GetArgument(object? argumentValue, Type? argumentType)
{
case null:
return "null";
- case IParam iparam:
- return GetArgument(iparam.Value, argumentType);
case object[] array when array.Length == 1:
return GetArgument(array[0], argumentType);
case string text:
diff --git a/src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs b/src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs
new file mode 100644
index 0000000000..0c946f1c0e
--- /dev/null
+++ b/src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs
@@ -0,0 +1,40 @@
+#if !NET10_0_OR_GREATER
+namespace System.Linq;
+
+// System.Linq.AsyncEnumerable ships in the shared framework from .NET 10. BenchmarkDotNet provides the two members it
+// uses rather than taking the package, because the rest of that surface captures the ambient SynchronizationContext,
+// which BenchmarkDotNet must never do while its pump owns the calling thread. See CompositeValidator.ValidateAsync.
+internal static class AsyncEnumerable
+{
+ public static IAsyncEnumerable Empty()
+ => EmptyAsyncEnumerable.Instance;
+
+ public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ return FromIterator(source);
+
+ static async IAsyncEnumerable FromIterator(IEnumerable source)
+ {
+ foreach (TSource element in source)
+ {
+ yield return element;
+ }
+ }
+ }
+
+ private sealed class EmptyAsyncEnumerable : IAsyncEnumerable, IAsyncEnumerator
+ {
+ public static readonly EmptyAsyncEnumerable Instance = new();
+
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => this;
+
+ public TSource Current => default!;
+
+ public ValueTask MoveNextAsync() => new(false);
+
+ public ValueTask DisposeAsync() => default;
+ }
+}
+#endif
diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs
index 1bfaf346ce..ee6fcd2577 100644
--- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs
+++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs
@@ -7,6 +7,9 @@ namespace BenchmarkDotNet.Extensions
{
internal static class ReflectionExtensions
{
+ // The name the compiler gives an `implicit operator`; there is no reflection API that spells it.
+ internal const string OpImplicitMethodName = "op_Implicit";
+
internal static T? ResolveAttribute(this Type? type) where T : Attribute =>
type?.GetTypeInfo().GetCustomAttributes(typeof(T), false).OfType().FirstOrDefault();
@@ -16,21 +19,16 @@ internal static class ReflectionExtensions
internal static bool HasAttribute(this MemberInfo? memberInfo) where T : Attribute =>
memberInfo.ResolveAttribute() != null;
- internal static bool IsNullable(this Type type) => Nullable.GetUnderlyingType(type) != null;
-
- public static bool IsInitOnly(this PropertyInfo propertyInfo)
- {
- var setMethodReturnParameter = propertyInfo.SetMethod?.ReturnParameter;
- if (setMethodReturnParameter == null)
- return false;
-
- var isExternalInitType = typeof(System.Runtime.CompilerServices.Unsafe).Assembly
- .GetType("System.Runtime.CompilerServices.IsExternalInit");
- if (isExternalInitType == null)
- return false;
+ ///
+ /// The value to pass for an omitted optional argument. A parameter can be optional without declaring a
+ /// default ([Optional] with no [DefaultParameterValue]); null is right for those, because Invoke converts it
+ /// to default(T) even for a value type. Type.Missing is not: Invoke(object, object[]) does no
+ /// optional-parameter binding and rejects it.
+ ///
+ internal static object? GetDefaultArgumentValue(this ParameterInfo parameter)
+ => parameter.HasDefaultValue ? parameter.DefaultValue : null;
- return setMethodReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType);
- }
+ internal static bool IsNullable(this Type type) => Nullable.GetUnderlyingType(type) != null;
///
/// returns type name which can be used in generated C# code
@@ -210,13 +208,14 @@ private static MethodInfo[] GetBenchmarks(this TypeInfo typeInfo)
.Where(method => method.GetCustomAttributes(true).OfType().Any())
.ToArray();
- internal static (string Name, TAttribute Attribute, bool IsStatic, Type ParameterType)[]
+ internal static (string Name, TAttribute Attribute, bool IsStatic, Type ParameterType, MemberInfo Member)[]
GetTypeMembersWithGivenAttribute(this Type type, BindingFlags reflectionFlags)
where TAttribute : Attribute
{
var fields = type
.GetFields(reflectionFlags)
.Select(f => Create(
+ f,
f.Name,
f.ResolveAttribute(),
f.IsStatic,
@@ -225,25 +224,51 @@ internal static (string Name, TAttribute Attribute, bool IsStatic, Type Paramete
var properties = type
.GetProperties(reflectionFlags)
.Select(p => Create(
+ p,
p.Name,
p.ResolveAttribute(),
p.GetSetMethod()?.IsStatic == true,
p.PropertyType));
- return fields.Concat(properties)
- .WhereNotNull()
- .Select(x => x!.Value)
+ // One entry per name, keeping the most derived declaration. GetFields hands back a hidden base field
+ // alongside the `new` one hiding it - GetProperties collapses the pair, so only fields arrive twice -
+ // and the name binds to the most derived declaration everywhere it is then used. A second entry
+ // becomes a second parameter of the same name: it multiplies the cases against itself and emits the
+ // name twice in the runnable's object initializer, which is CS1912.
+ var found = new List<(MemberInfo Member, string Name, TAttribute Attribute, bool IsStatic, Type MemberType)>();
+ var indexByName = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var candidate in fields.Concat(properties).WhereNotNull().Select(x => x!.Value))
+ {
+ if (!indexByName.TryGetValue(candidate.Name, out int index))
+ {
+ indexByName.Add(candidate.Name, found.Count);
+ found.Add(candidate);
+ }
+ else if (found[index].Member.DeclaringType!.IsAssignableFrom(candidate.Member.DeclaringType))
+ {
+ found[index] = candidate;
+ }
+ }
+
+ return found
+ .Select(x => (x.Name, x.Attribute, x.IsStatic, x.MemberType, x.Member))
.ToArray();
- static (string Name, TAttribute Attribute, bool IsStatic, Type MemberType)?
- Create(string name, TAttribute? attribute, bool isStatic, Type memberType)
+ static (MemberInfo Member, string Name, TAttribute Attribute, bool IsStatic, Type MemberType)?
+ Create(MemberInfo member, string name, TAttribute? attribute, bool isStatic, Type memberType)
{
if (attribute == null)
return null;
- return (name, attribute, isStatic, memberType);
+ return (member, name, attribute, isStatic, memberType);
}
}
+ // What a parameter takes, ref/in/out set aside: reflection reports those as byref types - `ref T` is `T&`,
+ // which nothing is castable to - though the modifier says how the argument travels, not what it is.
+ internal static Type WithoutRefModifier(this Type parameterType)
+ => parameterType.IsByRef ? parameterType.GetElementType()! : parameterType;
+
internal static bool IsStackOnlyWithImplicitCast(this Type argumentType, [NotNullWhen(true)] object? argumentInstance)
{
if (argumentInstance == null)
@@ -254,17 +279,26 @@ internal static bool IsStackOnlyWithImplicitCast(this Type argumentType, [NotNul
var instanceType = argumentInstance.GetType();
- var implicitCastsDefinedInArgumentInstance = instanceType.GetMethods().Where(method => method.Name == "op_Implicit" && method.GetParameters().Any()).ToArray();
- if (implicitCastsDefinedInArgumentInstance.Any(implicitCast => implicitCast.ReturnType == argumentType && implicitCast.GetParameters().All(p => p.ParameterType == instanceType)))
- return true;
-
- var implicitCastsDefinedInArgumentType = argumentType.GetMethods().Where(method => method.Name == "op_Implicit" && method.GetParameters().Any()).ToArray();
- if (implicitCastsDefinedInArgumentType.Any(implicitCast => implicitCast.ReturnType == argumentType && implicitCast.GetParameters().All(p => p.ParameterType == instanceType)))
- return true;
-
- return false;
+ return HasImplicitConversion(argumentType, instanceType);
}
+ private static bool HasImplicitConversion(Type targetType, Type sourceType)
+ => DeclaresConversion(sourceType, targetType, sourceType)
+ || DeclaresConversion(targetType, targetType, sourceType);
+
+ // An `implicit operator` written for exactly these types. Only exactly: a source is admitted by naming
+ // what the parameter takes, so there is no conversion to reason about on the way into the operator - and
+ // reasoning about one is what reflection cannot do, since it answers the CLR's rules rather than C#'s.
+ //
+ // C# gathers operators from both types and their base classes. Reflection withholds a base's statics
+ // without FlattenHierarchy, so without it an operator inherited from a base is invisible.
+ private static bool DeclaresConversion(Type declaringType, Type targetType, Type sourceType)
+ => declaringType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
+ .Any(method => method.Name == OpImplicitMethodName
+ && method.ReturnType == targetType
+ && method.GetParameters() is { Length: 1 } parameters
+ && parameters[0].ParameterType == sourceType);
+
private static bool IsRunnableGenericType(TypeInfo typeInfo)
=> // if it is an open generic - there must be GenericBenchmark attributes
(!typeInfo.IsGenericTypeDefinition || typeInfo.GenericTypeArguments.Any() || typeInfo.GetCustomAttributes(true).OfType().Any())
@@ -309,17 +343,12 @@ internal static bool IsAwaitable(this Type type, [NotNullWhen(true)] out Awaitab
internal static bool IsAsyncEnumerable(this Type type, [NotNullWhen(true)] out AsyncEnumerableInfo? info)
{
- // 1. Pattern first: a public instance GetAsyncEnumerator with all-optional parameters whose
- // return type has a public instance MoveNextAsync awaitable-to-bool (also accepting
- // all-optional params) and a public instance Current property. Roslyn's `await foreach`
- // binds to this in preference to the interface, so we mirror that order. The element type
- // comes from Current so it tracks what the compiler binds to, even if the type also
- // implements IAsyncEnumerable for a different U. (Extension GetAsyncEnumerator is not
- // handled.)
- //
- // Note: when the type IS exactly IAsyncEnumerable, `GetMethods(Public|Instance)` returns
- // the interface's own GetAsyncEnumerator, so this branch also handles that case naturally —
- // we just flag it as interface dispatch via the conditional below.
+ // 1. Pattern first, as `await foreach` binds: a public instance GetAsyncEnumerator with all-optional parameters,
+ // returning a type with a public MoveNextAsync awaitable-to-bool (also accepting all-optional params) and a
+ // public Current property. The element type comes from Current, so it tracks what the compiler binds to even
+ // when the type also implements IAsyncEnumerable for another U. Extension GetAsyncEnumerator is not handled.
+ // IAsyncEnumerable itself lands here too - GetMethods returns the interface's own - and the conditional
+ // below flags it as interface dispatch.
var patternGetAsyncEnumerator = type
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == nameof(IAsyncEnumerable<>.GetAsyncEnumerator)
@@ -357,21 +386,7 @@ internal static bool IsAsyncEnumerable(this Type type, [NotNullWhen(true)] out A
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
{
- var ifaceItemType = iface.GetGenericArguments()[0];
- var ifaceEnumeratorType = typeof(IAsyncEnumerator<>).MakeGenericType(ifaceItemType);
- var ifaceMoveNextAsync = ifaceEnumeratorType.GetMethod(nameof(IAsyncEnumerator<>.MoveNextAsync))!;
- // `MoveNextAsync` on `IAsyncEnumerator` returns `ValueTask` which always
- // satisfies the awaitable shape; pull the resolved `AwaitableInfo` from IsAwaitable
- // rather than constructing it by hand.
- ifaceMoveNextAsync.ReturnType.IsAwaitable(out var ifaceMoveNextAwaitable);
- info = new AsyncEnumerableInfo(
- ifaceItemType,
- ifaceEnumeratorType,
- iface.GetMethod(nameof(IAsyncEnumerable<>.GetAsyncEnumerator))!,
- ifaceMoveNextAsync,
- ifaceMoveNextAwaitable!,
- ifaceEnumeratorType.GetProperty(nameof(IAsyncEnumerator<>.Current))!,
- IsInterfaceDispatch: true);
+ info = GetAsyncEnumerableInterfaceInfo(iface.GetGenericArguments()[0]);
return true;
}
}
@@ -379,6 +394,170 @@ internal static bool IsAsyncEnumerable(this Type type, [NotNullWhen(true)] out A
return false;
}
+ // The await-foreach members of IAsyncEnumerable itself, for a caller that has already established the
+ // interface and wants it bound in preference to any pattern method the concrete type may also declare.
+ // Every member is the interface's own, so invoking them dispatches to the implementation whether it is
+ // implicit or explicit.
+ internal static AsyncEnumerableInfo GetAsyncEnumerableInterfaceInfo(Type elementType)
+ {
+ var interfaceType = typeof(IAsyncEnumerable<>).MakeGenericType(elementType);
+ var enumeratorType = typeof(IAsyncEnumerator<>).MakeGenericType(elementType);
+ var moveNextAsync = enumeratorType.GetMethod(nameof(IAsyncEnumerator<>.MoveNextAsync))!;
+ // `MoveNextAsync` on `IAsyncEnumerator` returns `ValueTask` which always satisfies the
+ // awaitable shape; pull the resolved `AwaitableInfo` from IsAwaitable rather than building it by hand.
+ moveNextAsync.ReturnType.IsAwaitable(out var moveNextAwaitable);
+ return new AsyncEnumerableInfo(
+ elementType,
+ enumeratorType,
+ interfaceType.GetMethod(nameof(IAsyncEnumerable<>.GetAsyncEnumerator))!,
+ moveNextAsync,
+ moveNextAwaitable!,
+ enumeratorType.GetProperty(nameof(IAsyncEnumerator<>.Current))!,
+ IsInterfaceDispatch: true);
+ }
+
+ // Whether the type is, or implements, IAsyncEnumerable. [ParamsSource]/[ArgumentsSource] async sources
+ // must use the interface (not just the await-foreach pattern), so callers reject pattern-only types.
+ internal static bool IsIAsyncEnumerable(this Type type, [NotNullWhen(true)] out Type? elementType)
+ {
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
+ {
+ elementType = type.GetGenericArguments()[0];
+ return true;
+ }
+ foreach (var iface in type.GetInterfaces())
+ {
+ if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
+ {
+ elementType = iface.GetGenericArguments()[0];
+ return true;
+ }
+ }
+ elementType = null;
+ return false;
+ }
+
+ // Mirrors AsyncTypeShapes.CountSourceShapes on the analyzer side. The generated extraction call infers its
+ // element type from the source, and type inference needs a *unique* candidate interface, so a source with
+ // anything other than exactly one instantiation across the two shapes fails to compile (CS0411) - even when
+ // one element type converts to the other, as with IEnumerable plus IEnumerable.
+ internal static int CountSourceShapes(this Type type)
+ => type.CountInstantiations(typeof(IEnumerable<>)) + type.CountInstantiations(typeof(IAsyncEnumerable<>));
+
+ private static int CountInstantiations(this Type type, Type interfaceDefinition)
+ {
+ var found = new HashSet();
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == interfaceDefinition)
+ found.Add(type);
+ foreach (var iface in type.GetInterfaces())
+ {
+ if (iface.IsGenericType && iface.GetGenericTypeDefinition() == interfaceDefinition)
+ found.Add(iface);
+ }
+ return found.Count;
+ }
+
+ // The element type a source declares, which is the type the generated extraction call returns and therefore
+ // the type the generated code indexes into. Only an unambiguous shape has one - see CountSourceShapes.
+ internal static bool TryGetSourceElementType(this Type sourceReturnType, [NotNullWhen(true)] out Type? elementType)
+ {
+ if (sourceReturnType.CountSourceShapes() == 1)
+ {
+ foreach (var candidate in sourceReturnType.GetInterfaces().Prepend(sourceReturnType))
+ {
+ if (candidate.IsGenericType
+ && (candidate.GetGenericTypeDefinition() == typeof(IEnumerable<>)
+ || candidate.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)))
+ {
+ elementType = candidate.GetGenericArguments()[0];
+ return true;
+ }
+ }
+ }
+ elementType = null;
+ return false;
+ }
+
+ // The member a [ParamsSource]/[ArgumentsSource] name resolves to: a public method whose parameters are all
+ // optional, else a property with a public getter. The single resolver - discovery reads its values from
+ // whatever this returns and SourceReturnTypeValidator reports on the same member, so the two cannot judge
+ // different members of the same name. A generic method definition is passed over rather than matched, so a
+ // property of that name serves the name instead of an unusable method.
+ internal static MemberInfo? FindSourceMember(this Type sourceType, string sourceName)
+ => (MemberInfo?) sourceType.GetAllMethods()
+ .FirstOrDefault(method => method.Name == sourceName && method.IsPublic
+ && !method.IsGenericMethodDefinition
+ && method.GetParameters().All(parameter => parameter.IsOptional))
+ ?? sourceType.GetAllProperties()
+ .FirstOrDefault(property => property.Name == sourceName && property.GetMethod?.IsPublic == true);
+
+ ///
+ /// The members a benchmark's parameters are looked for on. FlattenHierarchy is what reaches a base type's
+ /// statics - without it reflection returns inherited *instance* members only. Discovery and every validator
+ /// reporting on parameter members share this, so none can judge a member another cannot see. Not to be
+ /// confused with , which is its opposite.
+ ///
+ internal const BindingFlags ParameterMemberFlags =
+ BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
+
+ ///
+ /// The property or field a parameter is written through, matched on the declared type discovery recorded as
+ /// well as the name. These flags reach a hidden base member beside the one hiding it, and reflection returns
+ /// members in no particular order, so the most derived declaration is chosen here. The choice spans both
+ /// kinds: a field can hide a property, and looking for one kind first would find the base member of that
+ /// kind and never reach the derived member that is the parameter.
+ ///
+ internal static MemberInfo? GetParameterMember(this Type type, string name, Type parameterType, BindingFlags flags)
+ {
+ MemberInfo? found = null;
+ foreach (var member in type.GetMembers(flags))
+ {
+ var memberType = member switch
+ {
+ // An indexer takes arguments and is never a parameter member.
+ PropertyInfo property when property.GetIndexParameters().Length == 0 => property.PropertyType,
+ FieldInfo field => field.FieldType,
+ _ => null
+ };
+
+ if (memberType != parameterType || member.Name != name)
+ continue;
+ if (found is null || found.DeclaringType!.IsAssignableFrom(member.DeclaringType))
+ found = member;
+ }
+ return found;
+ }
+
+ // DeclaredOnly because the member sought is declared on the type being searched, and a metadata token is
+ // unique only within a module. Inherited members are what ParameterMemberFlags exists to reach.
+ private const BindingFlags DeclaredMemberFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly;
+
+ // The member as `contextType`'s generic definition names it - on that definition, or on the base in its
+ // hierarchy that declares it. The base is read as the derived type writes it, `Base` carrying the derived
+ // type's own T, so inherited and locally declared members share type parameters. Null when it is elsewhere.
+ internal static MemberInfo? GetDeclaredMemberIn(this MemberInfo member, Type contextType)
+ {
+ if (member.DeclaringType is not { } declaringType)
+ return null;
+
+ var declaringDefinition = declaringType.IsGenericType ? declaringType.GetGenericTypeDefinition() : declaringType;
+
+ for (var candidate = contextType.IsGenericType ? contextType.GetGenericTypeDefinition() : contextType; candidate != null; candidate = candidate.BaseType)
+ {
+ if ((candidate.IsGenericType ? candidate.GetGenericTypeDefinition() : candidate) != declaringDefinition)
+ continue;
+
+ return candidate.GetMembers(DeclaredMemberFlags)
+ .FirstOrDefault(inherited => inherited.MetadataToken == member.MetadataToken);
+ }
+
+ return null;
+ }
+
+ // The type a source member hands back, which is what the generated code infers the element type from.
+ internal static Type GetSourceReturnType(this MemberInfo source)
+ => source is PropertyInfo property ? property.GetMethod!.ReturnType : ((MethodInfo) source).ReturnType;
+
internal static Attribute? GetAsyncMethodBuilderAttribute(this MemberInfo memberInfo)
// AsyncMethodBuilderAttribute can come from any assembly, so we need to use reflection by name instead of searching for the exact type.
=> memberInfo.GetCustomAttributes(false).FirstOrDefault(attr => attr.GetType().FullName == typeof(AsyncMethodBuilderAttribute).FullName) as Attribute;
diff --git a/src/BenchmarkDotNet/Helpers/DisposeHelper.cs b/src/BenchmarkDotNet/Helpers/DisposeHelper.cs
new file mode 100644
index 0000000000..fb1ac81a8b
--- /dev/null
+++ b/src/BenchmarkDotNet/Helpers/DisposeHelper.cs
@@ -0,0 +1,31 @@
+using System.Runtime.ExceptionServices;
+
+namespace BenchmarkDotNet.Helpers;
+
+internal static class DisposeHelper
+{
+ public static async ValueTask DisposeAllAsync(this IEnumerable asyncDisposables)
+ {
+ List? exceptions = null;
+ foreach (var asyncDisposable in asyncDisposables)
+ {
+ try
+ {
+ await asyncDisposable.DisposeAsync().ConfigureAwait();
+ }
+ catch (Exception ex)
+ {
+ exceptions ??= [];
+ exceptions.Add(ex);
+ }
+ }
+ switch (exceptions)
+ {
+ case [var exception]:
+ ExceptionDispatchInfo.Capture(exception).Throw();
+ break; // unreachable
+ case not null:
+ throw new AggregateException(exceptions);
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs b/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs
index 0523f3ab6e..a312c89e98 100644
--- a/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs
+++ b/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs
@@ -1,5 +1,6 @@
using BenchmarkDotNet.Extensions;
using System.Reflection;
+using System.Runtime.ExceptionServices;
using System.Runtime.CompilerServices;
namespace BenchmarkDotNet.Helpers;
@@ -12,32 +13,45 @@ internal static class DynamicAwaitHelper
return (awaitableInfo.ResultType != typeof(void), result);
}
- internal static ValueTask DrainAsyncEnumerableAsync(object asyncEnumerable, AsyncEnumerableInfo enumerableInfo)
- => EnumerateCoreAsync(asyncEnumerable, enumerableInfo, items: null);
+ internal static IAsyncEnumerable EnumerateSourceAsync(object asyncEnumerable, Type elementType)
+ // Sources are always read through IAsyncEnumerable, pattern-based await foreach types are not supported.
+ // A reference-type element needs no reflection at all: IAsyncEnumerable is covariant.
+ => !elementType.IsValueType
+ ? (IAsyncEnumerable) asyncEnumerable
+ : EnumerateSourceAsyncCore(asyncEnumerable, ReflectionExtensions.GetAsyncEnumerableInterfaceInfo(elementType));
- internal static async ValueTask> ToListAsync(object asyncEnumerable, AsyncEnumerableInfo enumerableInfo)
+ private static async IAsyncEnumerable EnumerateSourceAsyncCore(object asyncEnumerable, AsyncEnumerableInfo asyncEnumerableInfo, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- List items = [];
- await EnumerateCoreAsync(asyncEnumerable, enumerableInfo, items).ConfigureAwait(false);
- return items;
+ var enumerator = Unwrapped(() => asyncEnumerableInfo.GetAsyncEnumeratorMethod.Invoke(asyncEnumerable, [cancellationToken]))!;
+ var moveNextAsyncArgs = GetDefaultArgs(asyncEnumerableInfo.MoveNextAsyncMethod);
+
+ try
+ {
+ while (await ((ValueTask) Unwrapped(() => asyncEnumerableInfo.MoveNextAsyncMethod.Invoke(enumerator, moveNextAsyncArgs))!).ConfigureAwait(false))
+ {
+ yield return Unwrapped(() => asyncEnumerableInfo.CurrentProperty.GetValue(enumerator));
+ }
+ }
+ finally
+ {
+ await ((IAsyncDisposable) enumerator).DisposeAsync().ConfigureAwait(false);
+ }
}
- private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncEnumerableInfo enumerableInfo, List? items)
+ internal static async IAsyncEnumerable EnumerateBenchmarkAsync(object asyncEnumerable, AsyncEnumerableInfo asyncEnumerableInfo)
{
- var getAsyncEnumeratorArgs = GetDefaultArgs(enumerableInfo.GetAsyncEnumeratorMethod);
- var enumerator = enumerableInfo.GetAsyncEnumeratorMethod.Invoke(asyncEnumerable, getAsyncEnumeratorArgs)!;
+ var enumerator = Unwrapped(() => asyncEnumerableInfo.GetAsyncEnumeratorMethod.Invoke(asyncEnumerable, GetDefaultArgs(asyncEnumerableInfo.GetAsyncEnumeratorMethod)))!;
- var moveNextAsyncArgs = GetDefaultArgs(enumerableInfo.MoveNextAsyncMethod);
- var currentProperty = enumerableInfo.CurrentProperty;
- var moveNextAwaitable = enumerableInfo.MoveNextAwaitable;
+ var moveNextAsyncArgs = GetDefaultArgs(asyncEnumerableInfo.MoveNextAsyncMethod);
+ var currentProperty = asyncEnumerableInfo.CurrentProperty;
+ var moveNextAwaitable = asyncEnumerableInfo.MoveNextAwaitable;
- // DisposeAsync is optional for the await-foreach pattern. Roslyn matches a public instance
- // method named DisposeAsync whose parameters are all optional and whose return type satisfies
- // the awaitable pattern with a void GetResult; otherwise it falls back to the IAsyncDisposable
- // interface dispatch.
+ // DisposeAsync is optional in the await-foreach pattern: Roslyn matches a public instance DisposeAsync
+ // whose parameters are all optional and whose return type is awaitable with a void GetResult, else the
+ // IAsyncDisposable interface.
MethodInfo? disposeAsyncMethod = null;
AwaitableInfo? disposeAwaitableInfo = null;
- foreach (var candidate in enumerableInfo.EnumeratorType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
+ foreach (var candidate in asyncEnumerableInfo.EnumeratorType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (candidate.Name == nameof(IAsyncDisposable.DisposeAsync)
&& candidate.GetParameters().All(p => p.IsOptional)
@@ -49,7 +63,7 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE
break;
}
}
- if (disposeAsyncMethod is null && typeof(IAsyncDisposable).IsAssignableFrom(enumerableInfo.EnumeratorType))
+ if (disposeAsyncMethod is null && typeof(IAsyncDisposable).IsAssignableFrom(asyncEnumerableInfo.EnumeratorType))
{
disposeAsyncMethod = typeof(IAsyncDisposable).GetMethod(nameof(IAsyncDisposable.DisposeAsync))!;
disposeAsyncMethod.ReturnType.IsAwaitable(out disposeAwaitableInfo);
@@ -60,20 +74,20 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE
{
while (true)
{
- var moveNextResult = enumerableInfo.MoveNextAsyncMethod.Invoke(enumerator, moveNextAsyncArgs);
+ var moveNextResult = Unwrapped(() => asyncEnumerableInfo.MoveNextAsyncMethod.Invoke(enumerator, moveNextAsyncArgs));
bool hasMore = (bool)(await new DynamicAwaitable(moveNextAwaitable, moveNextResult!))!;
if (!hasMore)
{
break;
}
- items?.Add(currentProperty.GetValue(enumerator));
+ yield return Unwrapped(() => currentProperty.GetValue(enumerator));
}
}
finally
{
if (disposeAsyncMethod != null)
{
- var disposeResult = disposeAsyncMethod.Invoke(enumerator, disposeAsyncArgs);
+ var disposeResult = Unwrapped(() => disposeAsyncMethod.Invoke(enumerator, disposeAsyncArgs));
if (disposeResult != null)
{
await new DynamicAwaitable(disposeAwaitableInfo!, disposeResult);
@@ -92,7 +106,7 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE
var args = new object?[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
- args[i] = parameters[i].HasDefaultValue ? parameters[i].DefaultValue : null;
+ args[i] = parameters[i].GetDefaultArgumentValue();
}
return args;
}
@@ -100,16 +114,51 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE
private readonly struct DynamicAwaitable(AwaitableInfo awaitableInfo, object awaitable)
{
public DynamicAwaiter GetAwaiter()
- => new(awaitableInfo, awaitableInfo.GetAwaiterMethod.Invoke(awaitable, null));
+ {
+ // Read into locals: a lambda in a struct's instance member cannot capture a primary constructor parameter.
+ var info = awaitableInfo;
+ object target = awaitable;
+
+ return new(info, Unwrapped(() => info.GetAwaiterMethod.Invoke(target, null)));
+ }
+ }
+
+ // Reflection wraps exceptions in TargetInvocationException, while the covariance path lets it through as thrown.
+ // Here we unwrap it and rethrow while preserving its stacktrace so both paths exceptions behave consistently.
+ private static object? Unwrapped(Func invoke)
+ {
+ try
+ {
+ return invoke();
+ }
+ catch (TargetInvocationException exception) when (exception.InnerException is { } inner)
+ {
+ ExceptionDispatchInfo.Capture(inner).Throw();
+ throw; // Not reached: Throw() always throws.
+ }
}
private readonly struct DynamicAwaiter(AwaitableInfo awaitableInfo, object? awaiter) : ICriticalNotifyCompletion
{
public bool IsCompleted
- => awaitableInfo.IsCompletedProperty.GetMethod!.Invoke(awaiter, null) is true;
+ {
+ get
+ {
+ var isCompleted = awaitableInfo.IsCompletedProperty.GetMethod!;
+ object? target = awaiter;
+
+ return Unwrapped(() => isCompleted.Invoke(target, null)) is true;
+ }
+ }
public object? GetResult()
- => awaitableInfo.GetResultMethod.Invoke(awaiter, null);
+ {
+ // Read into locals: a lambda in a struct's instance member cannot capture a primary constructor parameter.
+ var getResult = awaitableInfo.GetResultMethod;
+ object? target = awaiter;
+
+ return Unwrapped(() => getResult.Invoke(target, null));
+ }
public void OnCompleted(Action continuation)
=> OnCompletedCore(typeof(INotifyCompletion), nameof(INotifyCompletion.OnCompleted), continuation);
@@ -119,14 +168,41 @@ public void UnsafeOnCompleted(Action continuation)
private void OnCompletedCore(Type interfaceType, string methodName, Action continuation)
{
- var onCompletedMethod = interfaceType.GetMethod(methodName);
+ // ICriticalNotifyCompletion is optional in the awaiter pattern, but DynamicAwaiter declares it, so a
+ // state machine awaiting one always takes UnsafeOnCompleted. Asking GetInterfaceMap for an interface the
+ // user's awaiter does not implement throws, so hand those to OnCompleted - which flows the execution
+ // context that UnsafeOnCompleted exists to skip, the safe direction.
+ if (interfaceType == typeof(ICriticalNotifyCompletion)
+ && !typeof(ICriticalNotifyCompletion).IsAssignableFrom(awaitableInfo.AwaiterType))
+ {
+ OnCompletedCore(typeof(INotifyCompletion), nameof(INotifyCompletion.OnCompleted), continuation);
+ return;
+ }
+
+ var onCompletedMethod = interfaceType.GetMethod(methodName)!;
+
+ // The awaiter pattern binds on the awaiter's declared type, which may itself be an interface -
+ // GetInterfaceMap refuses one ("'this' type cannot be an interface itself"), and the throw lands on
+ // AwaitUnsafeOnCompleted, where it is rethrown onto the thread pool and takes the process down. No map
+ // is needed here anyway: invoking the interface method dispatches to whatever implements it, an
+ // explicit implementation included.
+ if (awaitableInfo.AwaiterType.IsInterface)
+ {
+ object? interfaceTarget = awaiter;
+ Unwrapped(() => onCompletedMethod.Invoke(interfaceTarget, [continuation]));
+ return;
+ }
+
var map = awaitableInfo.AwaiterType.GetInterfaceMap(interfaceType);
for (int i = 0; i < map.InterfaceMethods.Length; i++)
{
if (map.InterfaceMethods[i] == onCompletedMethod)
{
- map.TargetMethods[i].Invoke(awaiter, [continuation]);
+ var onCompleted = map.TargetMethods[i];
+ object? target = awaiter;
+
+ Unwrapped(() => onCompleted.Invoke(target, [continuation]));
return;
}
}
diff --git a/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs b/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs
index ea2916e00a..5d2f4571a2 100644
--- a/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs
+++ b/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs
@@ -8,6 +8,16 @@ namespace BenchmarkDotNet.Helpers
{
public static class SourceCodeHelper
{
+ ///
+ /// Renders a value as a C# expression, using to disambiguate the value.
+ /// An enum declared in F# is erased to its underlying type in attribute metadata, so the value alone
+ /// would render as a plain number (dotnet/fsharp#995).
+ ///
+ public static string ToSourceCode(object? value, Type declaredType)
+ => value != null && declaredType.IsEnum && !value.GetType().IsEnum
+ ? ToSourceCode(Enum.ToObject(declaredType, value))
+ : ToSourceCode(value);
+
public static string ToSourceCode(object? value)
{
switch (value)
@@ -65,7 +75,7 @@ public static string ToSourceCode(object? value)
return value.ToString()!;
}
- public static bool IsCompilationTimeConstant(object value)
+ public static bool IsCompilationTimeConstant(object? value)
=> value == null || IsCompilationTimeConstant(value.GetType());
public static bool IsCompilationTimeConstant(Type type)
diff --git a/src/BenchmarkDotNet/Parameters/ArrayDisplay.cs b/src/BenchmarkDotNet/Parameters/ArrayDisplay.cs
new file mode 100644
index 0000000000..cb5d10f99a
--- /dev/null
+++ b/src/BenchmarkDotNet/Parameters/ArrayDisplay.cs
@@ -0,0 +1,32 @@
+using BenchmarkDotNet.Extensions;
+
+namespace BenchmarkDotNet.Parameters;
+
+// Renders an array parameter's value for the Params column, e.g. "Int32[3]" or "Int32[2,2][]".
+internal static class ArrayDisplay
+{
+ public static string GetDisplayString(Array array)
+ {
+ string dimensionRepr = string.Join(", ", Enumerable.Range(0, array.Rank).Select(array.GetLength));
+
+ var (baseElementTypeRepr, innerDimensions) = GetDisplayString(array.GetType());
+
+ innerDimensions = string.Join("", innerDimensions.Split([']'], count: 2).Skip(1));
+
+ return $"{baseElementTypeRepr}[{dimensionRepr}]{innerDimensions}";
+ }
+
+ private static (string BaseElementTypeRepr, string InnerDimensions) GetDisplayString(Type arrayType)
+ {
+ var elemType = arrayType.GetElementType()!;
+
+ if (elemType.IsArray)
+ {
+ var (baseElementTypeRepr, innerDimensions) = GetDisplayString(elemType);
+
+ return (baseElementTypeRepr, $"[{new string(',', arrayType.GetArrayRank() - 1)}]{innerDimensions}");
+ }
+
+ return (elemType.GetDisplayName(), $"[{new string(',', arrayType.GetArrayRank() - 1)}]");
+ }
+}
diff --git a/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs b/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs
index 81295d964b..e450ca6218 100644
--- a/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs
+++ b/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs
@@ -1,22 +1,21 @@
-namespace BenchmarkDotNet.Parameters
+namespace BenchmarkDotNet.Parameters
{
- public class ParameterDefinition
+ ///
+ /// What a benchmark parameter is, independently of any value it takes: a member the runnable assigns, or a
+ /// parameter the benchmark method is called with.
+ ///
+ public sealed class ParameterDefinition(string name, bool isStatic, bool isArgument, Type parameterType, int priorityInCategory)
{
- public string Name { get; }
- public bool IsStatic { get; }
- public object?[] Values { get; }
- public bool IsArgument { get; }
- public Type ParameterType { get; }
- public int PriorityInCategory { get; }
+ public string Name { get; } = name;
- public ParameterDefinition(string name, bool isStatic, object?[] values, bool isArgument, Type parameterType, int priorityInCategory)
- {
- Name = name;
- IsStatic = isStatic;
- Values = values;
- IsArgument = isArgument;
- ParameterType = parameterType;
- PriorityInCategory = priorityInCategory;
- }
+ /// Whether the member holding the value is static. Always false for an argument, which is not a member.
+ public bool IsStatic { get; } = isStatic;
+
+ /// Whether the benchmark method is called with this parameter, rather than assigned it.
+ public bool IsArgument { get; } = isArgument;
+
+ public Type ParameterType { get; } = parameterType;
+
+ public int PriorityInCategory { get; } = priorityInCategory;
}
-}
\ No newline at end of file
+}
diff --git a/src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs b/src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs
deleted file mode 100644
index 1f8a1e54ff..0000000000
--- a/src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using BenchmarkDotNet.Extensions;
-using BenchmarkDotNet.Reports;
-using JetBrains.Annotations;
-
-namespace BenchmarkDotNet.Parameters
-{
- public class ParameterDefinitions
- {
- [PublicAPI] public IReadOnlyList Items { get; }
-
- public ParameterDefinitions(IReadOnlyList items) => Items = items;
-
- public IReadOnlyList Expand(SummaryStyle summaryStyle) => Expand([new ParameterInstances(new List())], Items, summaryStyle);
-
- private static IReadOnlyList Expand(IReadOnlyList instancesList, IReadOnlyList definitions, SummaryStyle summaryStyle)
- {
- if (definitions.IsNullOrEmpty())
- return instancesList;
- var nextDefinition = definitions.First();
- var newInstancesList = new List();
- foreach (var instances in instancesList)
- {
- foreach (var value in nextDefinition.Values)
- {
- var items = new List();
- items.AddRange(instances.Items);
- items.Add(new ParameterInstance(nextDefinition, value, summaryStyle));
- newInstancesList.Add(new ParameterInstances(items));
- }
- }
- return Expand(newInstancesList, definitions.Skip(1).ToArray(), summaryStyle);
- }
-
- public override string ToString() => Items.Any() ? string.Join(",", Items.Select(item => item.Name)) : "";
- }
-}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Parameters/ParameterInstance.cs b/src/BenchmarkDotNet/Parameters/ParameterInstance.cs
index 41f7b88555..36447d3836 100644
--- a/src/BenchmarkDotNet/Parameters/ParameterInstance.cs
+++ b/src/BenchmarkDotNet/Parameters/ParameterInstance.cs
@@ -1,56 +1,98 @@
-using BenchmarkDotNet.Code;
+using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
-using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Reports;
using JetBrains.Annotations;
using System.Globalization;
namespace BenchmarkDotNet.Parameters
{
- public class ParameterInstance : IDisposable
+ public class ParameterInstance : IDisposable, IAsyncDisposable
{
public const string NullParameterTextRepresentation = "?";
[PublicAPI] public ParameterDefinition Definition { get; }
- private readonly object? value;
+ ///
+ /// The value bound to this benchmark case, and the description a toolchain uses to re-create it in
+ /// generated code. The toolchain owns the generated syntax; this describes the value only.
+ ///
+ public ParameterValue ParameterValue { get; }
+
private readonly int maxParameterColumnWidthFromConfig;
- public ParameterInstance(ParameterDefinition definition, object? value, SummaryStyle? summaryStyle)
+ public ParameterInstance(ParameterDefinition definition, ParameterValue parameterValue, SummaryStyle? summaryStyle)
{
Definition = definition;
- this.value = value;
+ ParameterValue = parameterValue;
maxParameterColumnWidthFromConfig = summaryStyle?.MaxParameterColumnWidth ?? SummaryStyle.DefaultMaxParameterColumnWidth;
}
- public void Dispose() => (Value as IDisposable)?.Dispose();
+ /// Convenience overload for a value the toolchain can embed directly.
+ internal ParameterInstance(ParameterDefinition definition, object? value, SummaryStyle? summaryStyle)
+ : this(definition, new ParameterValue.Constant(value, definition.ParameterType), summaryStyle)
+ {
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ switch (Value)
+ {
+ case IAsyncDisposable asyncDisposable:
+ await asyncDisposable.DisposeAsync().ConfigureAwait(false);
+ break;
+
+ case IDisposable disposable:
+ disposable.Dispose();
+ break;
+ }
+ }
+
+ public void Dispose()
+ {
+ switch (Value)
+ {
+ // Intentionally flipped the order from DisposeAsync to avoid sync-over-async if the value already supports sync dispose.
+ case IDisposable disposable:
+ disposable.Dispose();
+ break;
+ case IAsyncDisposable asyncDisposable:
+ {
+ using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
+ context.ExecuteUntilComplete(asyncDisposable.DisposeAsync());
+ break;
+ }
+ }
+ }
public string Name => Definition.Name;
public bool IsStatic => Definition.IsStatic;
public bool IsArgument => Definition.IsArgument;
- public object? Value => value is IParam parameter ? parameter.Value : value;
-
- public string ToSourceCode()
- => value is IParam parameter
- ? parameter.ToSourceCode()
- : SourceCodeHelper.ToSourceCode(value);
+ public object? Value => ParameterValue.Value;
private string ToDisplayText(CultureInfo cultureInfo, int maxParameterColumnWidth)
{
- switch (value)
+ switch (Value)
{
case null:
return NullParameterTextRepresentation;
- case IParam parameter:
- return Trim(parameter.DisplayText, maxParameterColumnWidth).EscapeSpecialCharacters(false);
+ case Array array:
+ return Trim(ArrayDisplay.GetDisplayString(array), maxParameterColumnWidth).EscapeSpecialCharacters(false);
+ // An enum declared in F# is erased to its underlying type in attribute metadata, so the declared
+ // type is what names the member (dotnet/fsharp#995). Matched on that underlying type exactly,
+ // because a source can yield anything: any other value is a mismatch for someone else to report,
+ // not something to crash Enum.ToObject on while rendering a display name.
+ case var _ when Definition.ParameterType is { IsEnum: true }
+ && !Value!.GetType().IsEnum
+ && Value.GetType() == Enum.GetUnderlyingType(Definition.ParameterType):
+ return Trim(Enum.ToObject(Definition.ParameterType, Value).ToString()!, maxParameterColumnWidth).EscapeSpecialCharacters(false);
case IFormattable formattable:
return Trim(formattable.ToString(null, cultureInfo), maxParameterColumnWidth).EscapeSpecialCharacters(false);
// no trimming for types!
case Type type:
return type.IsNullable() ? $"{Nullable.GetUnderlyingType(type)!.GetDisplayName()}?" : type.GetDisplayName();
default:
- return Trim(value.ToString()!, maxParameterColumnWidth).EscapeSpecialCharacters(false);
+ return Trim(Value.ToString()!, maxParameterColumnWidth).EscapeSpecialCharacters(false);
}
}
diff --git a/src/BenchmarkDotNet/Parameters/ParameterInstances.cs b/src/BenchmarkDotNet/Parameters/ParameterInstances.cs
index 39d30c5c7a..2f679026d7 100644
--- a/src/BenchmarkDotNet/Parameters/ParameterInstances.cs
+++ b/src/BenchmarkDotNet/Parameters/ParameterInstances.cs
@@ -1,8 +1,10 @@
+using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
namespace BenchmarkDotNet.Parameters
{
- public class ParameterInstances : IEquatable, IDisposable
+ public class ParameterInstances : IDisposable, IAsyncDisposable
{
public static readonly ParameterInstances Empty = new([]);
@@ -11,19 +13,17 @@ public class ParameterInstances : IEquatable, IDisposable
public ParameterInstance this[int index] => Items[index];
public object? this[string name] => Items.FirstOrDefault(item => item.Name == name)?.Value;
- private string? printInfo = null;
-
public ParameterInstances(IReadOnlyList items)
{
Items = items;
}
+ public ValueTask DisposeAsync() => Items.DisposeAllAsync();
+
public void Dispose()
{
- foreach (var parameterInstance in Items)
- {
- parameterInstance.Dispose();
- }
+ using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
+ context.ExecuteUntilComplete(DisposeAsync());
}
public string FolderInfo => string.Join("_", Items.Select(p => $"{p.Name}-{p.ToDisplayText()}")).AsValidFileName();
@@ -32,45 +32,8 @@ public void Dispose()
public string ValueInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation}")) + "]" : "";
- public string PrintInfo => printInfo ?? (printInfo = string.Join("&", Items.Select(p => $"{p.Name}={p.ToDisplayText()}")));
+ public string PrintInfo => field ??= string.Join("&", Items.Select(p => $"{p.Name}={p.ToDisplayText()}"));
public ParameterInstance GetArgument(string name) => Items.Single(parameter => parameter.IsArgument && parameter.Name == name);
-
- public bool Equals(ParameterInstances? other)
- {
- if (ReferenceEquals(this, other))
- return true;
-
- if (other == null)
- return false;
-
- if (other.Count != Count)
- return false;
-
- if (Count != other.Count)
- return false;
-
- for (int i = 0; i < Count; i++)
- {
- var currentItem = Items[i];
- var otherItem = other.Items[i];
-
- if (ReferenceEquals(currentItem, otherItem))
- continue;
-
- if (currentItem is null || otherItem is null)
- return false;
-
- if (!currentItem.Equals(otherItem.Value))
- return false;
- }
-
- return true;
- }
-
- public override bool Equals(object? obj)
- => obj is ParameterInstances other && Equals(other);
-
- public override int GetHashCode() => FolderInfo.GetHashCode();
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Parameters/ParameterValue.cs b/src/BenchmarkDotNet/Parameters/ParameterValue.cs
new file mode 100644
index 0000000000..c8c404ff1f
--- /dev/null
+++ b/src/BenchmarkDotNet/Parameters/ParameterValue.cs
@@ -0,0 +1,39 @@
+namespace BenchmarkDotNet.Parameters;
+
+///
+/// One value a benchmark parameter can take, and a language-neutral description for a toolchain to re-create it in generated code.
+///
+public abstract class ParameterValue
+{
+ /// The value of the parameter.
+ public object? Value { get; }
+
+ private ParameterValue(object? value) => Value = value;
+
+ /// A value the toolchain can embed directly, e.g. a primitive, string, enum, array, or .
+ public sealed class Constant(object? value, Type type) : ParameterValue(value)
+ {
+ /// The declared type of the parameter.
+ ///
+ /// Needed because the value alone can be ambiguous - an enum declared in F# is erased to its underlying type in attribute metadata (dotnet/fsharp#995).
+ ///
+ public Type Type { get; } = type;
+ }
+
+ ///
+ /// A value the toolchain cannot embed, so the generated code re-obtains it by enumerating the [ParamsSource]/[ArgumentsSource] member it originally came from.
+ ///
+ public sealed class FromSource(object? value, SourceRead read, int? elementIndex, Type targetType) : ParameterValue(value)
+ {
+ /// The read this value comes out of, shared by every parameter bound from the same one.
+ public SourceRead Read { get; } = read;
+
+ ///
+ /// The index of the value within the yielded element, if the element is an args-list array; otherwise.
+ ///
+ public int? ElementIndex { get; } = elementIndex;
+
+ /// The type the obtained value is used as.
+ public Type TargetType { get; } = targetType;
+ }
+}
diff --git a/src/BenchmarkDotNet/Parameters/ParameterValues.cs b/src/BenchmarkDotNet/Parameters/ParameterValues.cs
new file mode 100644
index 0000000000..de8d19fc44
--- /dev/null
+++ b/src/BenchmarkDotNet/Parameters/ParameterValues.cs
@@ -0,0 +1,17 @@
+namespace BenchmarkDotNet.Parameters
+{
+ ///
+ /// A parameter and the values it takes; the benchmark cases are the cartesian product of these.
+ ///
+ ///
+ /// Only a [Params], [ParamsSource] or [ParamsAllValues] member ranges over values this way. An argument is
+ /// filled from a row spanning the whole parameter list, so its values belong to the row and there is nothing
+ /// to pair with the definition.
+ ///
+ public sealed class ParameterValues(ParameterDefinition definition, IReadOnlyList items)
+ {
+ public ParameterDefinition Definition { get; } = definition;
+
+ public IReadOnlyList Items { get; } = items;
+ }
+}
diff --git a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs
index d204fb8e62..a23ed1b6f6 100644
--- a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs
+++ b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs
@@ -1,188 +1,148 @@
-using BenchmarkDotNet.Code;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+using JetBrains.Annotations;
using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace BenchmarkDotNet.Parameters
{
internal static class SmartParamBuilder
{
- [SuppressMessage("ReSharper", "CoVariantArrayConversion")]
- internal static object[] CreateForParams(Type parameterType, MemberInfo source, object[] values)
+ internal static IReadOnlyList CreateForParams(Type parameterType, MemberInfo source, object?[] values)
{
- // IEnumerable
- if (values.IsEmpty() || values.All(SourceCodeHelper.IsCompilationTimeConstant))
- return values;
-
- // IEnumerable
- if (values.All(value => value is object[] array && array.Length == 1 && SourceCodeHelper.IsCompilationTimeConstant(array[0])))
- return values.Select(x => ((object[])x)[0]).ToArray();
-
- return values.Select((value, index) => new SmartParameter(parameterType, source, value, index)).ToArray();
+ // A one-element object[] around a constant is unwrapped to the constant, which is then rendered inline
+ // and needs no index. Only around a constant: a value that has to be read back from the source keeps
+ // the whole array, because the generated code emits no index here to reach inside one with, and
+ // unwrapping only the in-process side would have the two toolchains assign different values.
+ if (values.All(value => value is object[] { Length: 1 } wrapper && SourceCodeHelper.IsCompilationTimeConstant(wrapper[0])))
+ values = values.Select(value => ((object[]) value!)[0]).ToArray();
+
+ return values.Select((value, index) =>
+ SourceCodeHelper.IsCompilationTimeConstant(value)
+ ? (ParameterValue) new ParameterValue.Constant(value, parameterType)
+ : new ParameterValue.FromSource(value, new SourceRead(source, index), elementIndex: null, parameterType)).ToArray();
}
- internal static ParameterInstances CreateForArguments(MethodInfo benchmark, ParameterDefinition[] parameterDefinitions, (MemberInfo source, object[] values) valuesInfo, int sourceIndex, SummaryStyle summaryStyle)
+ internal static ParameterInstances CreateForArguments(
+ MethodInfo benchmark,
+ ParameterDefinition[] parameterDefinitions,
+ (MemberInfo source, object?[] values) valuesInfo,
+ int sourceIndex,
+ SummaryStyle summaryStyle)
{
var unwrappedValue = valuesInfo.values[sourceIndex];
+ // One read for the whole row: every parameter below takes its value out of this one, so the generated
+ // code enumerates the source once per case rather than once per argument.
+ var read = new SourceRead(valuesInfo.source, sourceIndex);
+
if (unwrappedValue is object[] array)
{
- Type? firstParamType = benchmark.GetParameters().FirstOrDefault()?.ParameterType;
- // the user provided object[] for a benchmark accepting a single argument
- if (parameterDefinitions.Length == 1 && array.Length == 1
- && (array[0]?.GetType() == firstParamType || (firstParamType != null && firstParamType.IsStackOnlyWithImplicitCast(array[0])))) // the benchmark that accepts an object[] as argument
+ var firstParameterType = parameterDefinitions.FirstOrDefault()?.ParameterType;
+
+ // An object[] for a benchmark taking a single argument: the one that takes the array itself, or a
+ // by-ref-like one built from it.
+ if (parameterDefinitions.Length == 1 && array.Length == 1 && firstParameterType is not null
+ && (array[0]?.GetType() == firstParameterType || firstParameterType.IsStackOnlyWithImplicitCast(array[0])))
{
return new ParameterInstances(
- [Create(parameterDefinitions, array[0], valuesInfo.source, sourceIndex, argumentIndex: 0, summaryStyle)]);
+ [Create(parameterDefinitions, array[0], read, argumentIndex: 0, summaryStyle)]);
}
if (parameterDefinitions.Length > 1)
{
if (parameterDefinitions.Length != array.Length)
- throw new InvalidOperationException($"Benchmark {benchmark.Name} has invalid number of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]! {array.Length} instead of {parameterDefinitions.Length}.");
+ throw new InvalidOperationException($"Benchmark {benchmark.Name} has invalid number of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]!" +
+ $" {array.Length} instead of {parameterDefinitions.Length}.");
return new ParameterInstances(
- array.Select((value, argumentIndex) => Create(parameterDefinitions, value, valuesInfo.source, sourceIndex, argumentIndex, summaryStyle)).ToArray());
+ array.Select((value, argumentIndex) => Create(parameterDefinitions, value, read, argumentIndex, summaryStyle))
+ .ToArray());
}
}
if (parameterDefinitions.Length == 1)
{
- return new ParameterInstances([Create(parameterDefinitions, unwrappedValue, valuesInfo.source, sourceIndex, argumentIndex: 0, summaryStyle)]);
+ return new ParameterInstances([Create(parameterDefinitions, unwrappedValue, read, argumentIndex: 0, summaryStyle)]);
}
- throw new NotSupportedException($"Benchmark {benchmark.Name} has invalid type of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]. It should be IEnumerable or IEnumerable.");
+ throw new NotSupportedException($"Benchmark {benchmark.Name} has invalid type of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]." +
+ $" It should be IEnumerable, IEnumerable, IAsyncEnumerable or IAsyncEnumerable.");
}
- private static ParameterInstance Create(ParameterDefinition[] parameterDefinitions, object value, MemberInfo source, int sourceIndex, int argumentIndex, SummaryStyle summaryStyle)
+ // Whether the generated code indexes the element it extracts, decided by the interface the source is
+ // *written as* and not by which branch above produced this instance: the extraction call is bound against
+ // the declared return type, so the index has to be read from the same place the binding is.
+ private static bool Indexes(MemberInfo source)
{
- if (SourceCodeHelper.IsCompilationTimeConstant(value))
- return new ParameterInstance(parameterDefinitions[argumentIndex], value, summaryStyle);
+ var returnType = source.GetSourceReturnType();
- return new ParameterInstance(parameterDefinitions[argumentIndex], new SmartArgument(parameterDefinitions, value, source, sourceIndex, argumentIndex), summaryStyle);
- }
- }
-
- internal class SmartArgument : IParam
- {
- private readonly ParameterDefinition[] parameterDefinitions;
- private readonly MemberInfo source;
- private readonly int sourceIndex;
- private readonly int argumentIndex;
-
- public SmartArgument(ParameterDefinition[] parameterDefinitions, object value, MemberInfo source, int sourceIndex, int argumentIndex)
- {
- this.parameterDefinitions = parameterDefinitions;
- Value = value;
- this.source = source;
- this.sourceIndex = sourceIndex;
- this.argumentIndex = argumentIndex;
+ return returnType == typeof(IEnumerable) || returnType == typeof(IAsyncEnumerable);
}
- public object Value { get; }
-
- public string DisplayText => Value is Array array ? ArrayParam.GetDisplayString(array) : Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation;
-
- public string ToSourceCode()
+ private static ParameterInstance Create(ParameterDefinition[] parameterDefinitions, object? value, SourceRead read, int argumentIndex, SummaryStyle summaryStyle)
{
- Type paramType = parameterDefinitions[argumentIndex].ParameterType;
-
- // it's an object so we need to cast it to the right type
- string cast = paramType.IsByRefLike()
- ? $"({Value.GetType().GetCorrectCSharpTypeName()})"
- : $"({paramType.GetCorrectCSharpTypeName()})";
-
- string callPostfix = source is PropertyInfo ? string.Empty : "()";
-
- MethodInfo? sourceAsMethodInfo = source as MethodInfo;
- PropertyInfo? sourceAsPropertyInfo = source as PropertyInfo;
-
- Type indexableType = typeof(IEnumerable);
-
- string indexPostfix;
- if (sourceAsMethodInfo?.ReturnType == indexableType ||
- sourceAsPropertyInfo?.GetMethod?.ReturnType == indexableType)
- {
- indexPostfix = $"[{argumentIndex}]";
- }
- else
- {
- indexPostfix = string.Empty; // IEnumerable
- }
+ var definition = parameterDefinitions[argumentIndex];
+
+ // Asked ahead of the constant path, which renders the value into the source and needs the conversion
+ // just as much. Null is left to it: a ref struct is not written as one, and how that is rendered is
+ // settled there.
+ var takesByRefLike = definition.ParameterType.WithoutRefModifier();
+
+ // InvalidBenchmarkDeclarationException, not InvalidOperationException: BenchmarkRunnerDirty catches this
+ // one and reports it as that benchmark's summary, where anything else escapes Run and takes every other
+ // benchmark in the call down with it.
+ if (value is not null && takesByRefLike.IsByRefLike() && !takesByRefLike.IsStackOnlyWithImplicitCast(value))
+ throw new InvalidBenchmarkDeclarationException($"[ArgumentsSource({read.Source.Name})] provides a {value!.GetType().GetDisplayName()}" +
+ $" for the {definition.ParameterType.GetDisplayName()} parameter '{definition.Name}', which has no implicit conversion from it." +
+ $" A by-ref-like parameter only ever takes its value through that conversion, so nothing can be cast to it here." +
+ $" Please, yield a type it converts from - and where the source's element type is a type parameter," +
+ $" the [GenericTypeArguments] in play decide this, so it can hold for one and not the next.");
- string methodCall;
- if (sourceAsMethodInfo?.IsStatic ?? sourceAsPropertyInfo?.GetMethod?.IsStatic ?? throw new Exception($"{nameof(source)} was not {nameof(MethodInfo)} nor {nameof(PropertyInfo)}"))
- {
- // If the source member is static, we need to place the fully qualified type name before it, in case the source member is from another type that this generated type does not inherit from.
- methodCall = $"{source.DeclaringType!.GetCorrectCSharpTypeName()}.{source.Name}";
- }
- else
- {
- // If the source member is non-static, we mustn't include the type name, as this would be a compiler error when accessing a non-static source member in the base class of this generated type.
- methodCall = $"base.{source.Name}";
- }
-
- // we do something like enumerable.ElementAt(sourceIndex)[argumentIndex];
- return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({methodCall}{callPostfix}, {sourceIndex}){indexPostfix};";
- }
- }
-
- internal class SmartParameter : IParam
- {
- private readonly Type parameterType;
- private readonly MemberInfo source;
- private readonly MethodBase method;
- private readonly int index;
-
- public SmartParameter(Type parameterType, MemberInfo source, object value, int index)
- {
- this.parameterType = parameterType;
- this.source = source;
- method = source is PropertyInfo property ? property.GetMethod! : (MethodInfo)source;
- Value = value;
- this.index = index;
- }
-
- public object Value { get; }
-
- public string DisplayText => Value is Array array ? ArrayParam.GetDisplayString(array) : Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation;
-
- public string ToSourceCode()
- {
- string cast = $"({parameterType.GetCorrectCSharpTypeName()})"; // it's an object so we need to cast it to the right type
-
- string callPrefix = method.IsStatic ? source.DeclaringType!.GetCorrectCSharpTypeName() : "base";
+ if (SourceCodeHelper.IsCompilationTimeConstant(value))
+ return new ParameterInstance(definition, new ParameterValue.Constant(value, definition.ParameterType), summaryStyle);
- string callPostfix = source is PropertyInfo ? string.Empty : "()";
+ // A by-ref-like parameter can't be the source's element type, so the value's own type is the one the
+ // generated code casts to, relying on its implicit conversion (#774).
+ // value is non-null here: null is a compilation-time constant, handled above.
+ var targetType = takesByRefLike.IsByRefLike() ? value!.GetType() : definition.ParameterType;
- // we so something like enumerable.ElementAt(index);
- return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({callPrefix}.{source.Name}{callPostfix}, {index});";
+ return new ParameterInstance(
+ definition,
+ new ParameterValue.FromSource(value, read, Indexes(read.Source) ? argumentIndex : null, targetType),
+ summaryStyle);
}
}
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [UsedImplicitly]
public static class ParameterExtractor
{
- [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public so we can call it form the boilerplate code
- public static T GetParameter(IEnumerable parameters, int index)
+ public static ValueTask GetParameterAsync(IEnumerable parameters, int index, CancellationToken cancellationToken)
+ => GetParameterAsync(parameters.ToAsyncEnumerable(), index, cancellationToken);
+
+ public static async ValueTask GetParameterAsync(IAsyncEnumerable parameters, int index, CancellationToken cancellationToken)
{
int count = 0;
- foreach (T parameter in parameters)
+#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
+ await foreach (T parameter in parameters.ConfigureAwait(cancellationToken))
+#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
{
if (count == index)
{
return parameter;
}
- if (parameter is IDisposable disposable)
+ // #1383
+ if (parameter is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync().ConfigureAwait();
+ }
+ else if (parameter is IDisposable disposable)
{
- // parameters might contain locking finalizers which might cause the benchmarking process to hung at the end
- // to avoid that, we dispose the parameters that were created, but won't be used
- // (for every test case we have to enumerate the underlying source enumerator and stop when we reach index of given test case)
- // See https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more
disposable.Dispose();
}
diff --git a/src/BenchmarkDotNet/Parameters/SourceRead.cs b/src/BenchmarkDotNet/Parameters/SourceRead.cs
new file mode 100644
index 0000000000..f24a03268f
--- /dev/null
+++ b/src/BenchmarkDotNet/Parameters/SourceRead.cs
@@ -0,0 +1,22 @@
+using System.Reflection;
+
+namespace BenchmarkDotNet.Parameters;
+
+///
+/// One read of a [ParamsSource]/[ArgumentsSource] member: the member, and which of the values it yields.
+///
+///
+/// An arguments row is a single read that every parameter in it takes a value out of, so they share one instance
+/// of this and differ only in . A toolchain can therefore emit
+/// the read once and index into it, instead of recognising after the fact that several parameters would have
+/// re-read the same member at the same index. A [ParamsSource] value is a read of its own: members range over
+/// their values independently, so nothing is shared between them.
+///
+public sealed class SourceRead(MemberInfo source, int valueIndex)
+{
+ /// The source member (method or property) to enumerate.
+ public MemberInfo Source { get; } = source;
+
+ /// Index of the value within the sequence the source yields.
+ public int ValueIndex { get; } = valueIndex;
+}
diff --git a/src/BenchmarkDotNet/Running/BenchmarkCase.cs b/src/BenchmarkDotNet/Running/BenchmarkCase.cs
index ca64cde189..9aab68bae8 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkCase.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkCase.cs
@@ -10,7 +10,7 @@
namespace BenchmarkDotNet.Running
{
- public class BenchmarkCase : IComparable, IDisposable
+ public class BenchmarkCase : IComparable, IDisposable, IAsyncDisposable
{
public Descriptor Descriptor { get; }
public Job Job { get; }
@@ -48,6 +48,8 @@ internal IToolchain GetToolchain()
return GetRuntime().GetDefaultToolchain(this);
}
+ public ValueTask DisposeAsync() => Parameters.DisposeAsync();
+
public void Dispose() => Parameters.Dispose();
public int CompareTo(BenchmarkCase? other)
diff --git a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs
index bcc40e93ad..77ca206be2 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs
@@ -1,8 +1,10 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Code;
using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Filters;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Parameters;
using BenchmarkDotNet.Reports;
using System.Collections;
@@ -16,6 +18,12 @@ public static class BenchmarkConverter
private const BindingFlags AllMethodsFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
public static BenchmarkRunInfo TypeToBenchmarks(Type type, IConfig? config = null)
+ {
+ using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
+ return context.ExecuteUntilComplete(TypeToBenchmarksAsync(type, config));
+ }
+
+ public static ValueTask TypeToBenchmarksAsync(Type type, IConfig? config = null, CancellationToken cancellationToken = default)
{
if (type.IsGenericTypeDefinition)
throw new InvalidBenchmarkDeclarationException($"{type.Name} is generic type definition, use BenchmarkSwitcher for it"); // for "open generic types" should be used BenchmarkSwitcher
@@ -23,11 +31,17 @@ public static BenchmarkRunInfo TypeToBenchmarks(Type type, IConfig? config = nul
// We should check all methods including private to notify users about private methods with the [Benchmark] attribute
var benchmarkMethods = GetOrderedBenchmarkMethods(type.GetMethods(AllMethodsFlags));
- return MethodsToBenchmarksWithFullConfig(type, benchmarkMethods, config);
+ return MethodsToBenchmarksWithFullConfig(type, benchmarkMethods, config, cancellationToken);
}
public static BenchmarkRunInfo MethodsToBenchmarks(Type containingType, MethodInfo[] benchmarkMethods, IConfig? config = null)
- => MethodsToBenchmarksWithFullConfig(containingType, GetOrderedBenchmarkMethods(benchmarkMethods), config);
+ {
+ using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
+ return context.ExecuteUntilComplete(MethodsToBenchmarksAsync(containingType, benchmarkMethods, config));
+ }
+
+ public static ValueTask MethodsToBenchmarksAsync(Type containingType, MethodInfo[] benchmarkMethods, IConfig? config = null, CancellationToken cancellationToken = default)
+ => MethodsToBenchmarksWithFullConfig(containingType, GetOrderedBenchmarkMethods(benchmarkMethods), config, cancellationToken);
private static MethodInfo[] GetOrderedBenchmarkMethods(MethodInfo[] methods)
=> methods
@@ -38,7 +52,7 @@ private static MethodInfo[] GetOrderedBenchmarkMethods(MethodInfo[] methods)
.Select(pair => pair.method)
.ToArray();
- private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, MethodInfo[] benchmarkMethods, IConfig? config)
+ private static async ValueTask MethodsToBenchmarksWithFullConfig(Type type, MethodInfo[] benchmarkMethods, IConfig? config, CancellationToken cancellationToken)
{
var allMethods = type.GetMethods(AllMethodsFlags); // benchmarkMethods can be filtered, without Setups, look #564
var configPerType = GetFullTypeConfig(type, config);
@@ -51,8 +65,7 @@ private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, Met
var targets = GetTargets(benchmarkMethods, type, globalSetupMethods, globalCleanupMethods, iterationSetupMethods, iterationCleanupMethods,
configPerType).ToArray();
- var parameterDefinitions = GetParameterDefinitions(type);
- var parameterInstancesList = parameterDefinitions.Expand(configPerType.SummaryStyle);
+ var parameterInstances = await GetParameterInstancesAsync(type, configPerType.SummaryStyle, cancellationToken).ConfigureAwait();
var benchmarks = new List();
@@ -60,26 +73,26 @@ private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, Met
foreach (var target in targets)
{
- var argumentsDefinitions = GetArgumentsDefinitions(target.WorkloadMethod, target.Type, configPerType.SummaryStyle).ToArray();
+ var argumentsInstances = await GetArgumentsInstancesAsync(target.WorkloadMethod, target.Type, configPerType.SummaryStyle, cancellationToken).ConfigureAwait();
- var parameterInstances =
- (from parameterInstance in parameterInstancesList
- from argumentDefinition in argumentsDefinitions
- select new ParameterInstances(parameterInstance.Items.Concat(argumentDefinition.Items).ToArray())).ToArray();
+ var targetParameterInstances =
+ (from parameterInstance in parameterInstances
+ from argumentInstance in argumentsInstances
+ select new ParameterInstances([.. parameterInstance.Items, .. argumentInstance.Items])).ToArray();
var configPerMethod = GetFullMethodConfig(target.WorkloadMethod, configPerType);
var benchmarksForTarget =
- from job in configPerMethod.GetJobs()
- from parameterInstance in parameterInstances
- select BenchmarkCase.Create(target, job, parameterInstance, configPerMethod);
+ (from job in configPerMethod.GetJobs()
+ from parameterInstance in targetParameterInstances
+ select BenchmarkCase.Create(target, job, parameterInstance, configPerMethod)).ToArray();
- if (benchmarksForTarget.Any() && !containsBenchmarkDeclarations) containsBenchmarkDeclarations = true;
+ containsBenchmarkDeclarations |= benchmarksForTarget.Length != 0;
benchmarks.AddRange(GetFilteredBenchmarks(benchmarksForTarget, configPerMethod.GetFilters()));
}
- var orderedBenchmarks = configPerType.Orderer.GetExecutionOrder(benchmarks.ToImmutableArray()).ToArray();
+ var orderedBenchmarks = configPerType.Orderer.GetExecutionOrder([.. benchmarks]).ToArray();
var compositeInProcessDiagnoser = new Diagnosers.CompositeInProcessDiagnoser([.. configPerType.GetDiagnosers().OfType()]);
return new BenchmarkRunInfo(orderedBenchmarks, type, configPerType, containsBenchmarkDeclarations, compositeInProcessDiagnoser);
@@ -179,84 +192,98 @@ private static Descriptor CreateDescriptor(
return target;
}
- private static ParameterDefinitions GetParameterDefinitions(Type type)
+ private static async ValueTask> GetParameterInstancesAsync(Type type, SummaryStyle summaryStyle, CancellationToken cancellationToken)
{
- IEnumerable GetDefinitions(Func getValidValues) where TAttribute : PriorityAttribute
+ IEnumerable GetValues(Func> getValidValues) where TAttribute : PriorityAttribute
+ => type.GetTypeMembersWithGivenAttribute(ReflectionExtensions.ParameterMemberFlags)
+ .Select(member =>
+ new ParameterValues(
+ new(member.Name, member.IsStatic, isArgument: false, member.ParameterType, member.Attribute.Priority),
+ getValidValues(member.Attribute, member.ParameterType)
+ )
+ );
+
+ var parameters = GetValues((attribute, parameterType) => GetValidValues(attribute.Values, parameterType)).ToList();
+ foreach (var member in type.GetTypeMembersWithGivenAttribute(ReflectionExtensions.ParameterMemberFlags))
{
- const BindingFlags reflectionFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
-
- var allMembers = type.GetTypeMembersWithGivenAttribute(reflectionFlags);
- return allMembers.Select(member =>
- new ParameterDefinition(
- member.Name,
- member.IsStatic,
- getValidValues(member.Attribute, member.ParameterType),
- false,
- member.ParameterType,
- member.Attribute.Priority));
+ var targetType = member.Attribute.Type ?? type;
+ var (source, values) = await GetValidValuesForParamsSourceAsync(targetType, member.Attribute.Name, cancellationToken).ConfigureAwait();
+ parameters.Add(new ParameterValues(
+ new ParameterDefinition(member.Name, member.IsStatic, isArgument: false, member.ParameterType, member.Attribute.Priority),
+ SmartParamBuilder.CreateForParams(member.ParameterType, source, values)));
}
+ parameters.AddRange(GetValues((_, parameterType) => GetValidValues(GetAllValidValues(parameterType), parameterType)));
- var paramsDefinitions = GetDefinitions((attribute, parameterType) => GetValidValues(attribute.Values, parameterType));
-
- var paramsSourceDefinitions = GetDefinitions((attribute, parameterType) =>
+ // Each member ranges over its values independently, so the cases are their cartesian product: every case so far is re-made once per value
+ // of the next parameter. The seed is the single empty case, which is also the answer for a benchmark that has no parameters at all.
+ List cases = [ParameterInstances.Empty];
+ List? expanded = null;
+ foreach (var parameter in parameters)
{
- var targetType = attribute.Type ?? type;
-
- var paramsValues = GetValidValuesForParamsSource(targetType, attribute.Name);
- return SmartParamBuilder.CreateForParams(parameterType, paramsValues.source, paramsValues.values);
- });
-
- var paramsAllValuesDefinitions = GetDefinitions((_, parameterType) => GetAllValidValues(parameterType));
+ expanded ??= [];
+ expanded.Clear();
+#if NET6_0_OR_GREATER
+ expanded.EnsureCapacity(cases.Count * parameter.Items.Count);
+#endif
+ foreach (var instances in cases)
+ {
+ foreach (var value in parameter.Items)
+ {
+ expanded.Add(new ParameterInstances([.. instances.Items, new(parameter.Definition, value, summaryStyle)]));
+ }
+ }
+ (cases, expanded) = (expanded, cases);
+ }
- var definitions = paramsDefinitions.Concat(paramsSourceDefinitions).Concat(paramsAllValuesDefinitions).ToArray();
- return new ParameterDefinitions(definitions);
+ return cases;
}
- private static IEnumerable GetArgumentsDefinitions(MethodInfo benchmark, Type benchmarkType, SummaryStyle summaryStyle)
+ private static async ValueTask> GetArgumentsInstancesAsync(MethodInfo benchmark, Type benchmarkType, SummaryStyle summaryStyle, CancellationToken cancellationToken)
{
- var argumentsAttributes = benchmark.GetCustomAttributes();
- int priority = argumentsAttributes.Select(attribute => attribute.Priority).Sum();
+ int priority = benchmark.GetCustomAttributes().Sum(attribute => attribute.Priority);
var parameterDefinitions = benchmark.GetParameters()
- .Select(parameter => new ParameterDefinition(parameter.Name!, false, [], true, parameter.ParameterType, priority))
+ .Select(parameter => new ParameterDefinition(parameter.Name!, isStatic: false, isArgument: true, parameter.ParameterType, priority))
.ToArray();
- if (parameterDefinitions.IsEmpty())
+ if (parameterDefinitions.Length == 0)
{
- yield return new ParameterInstances([]);
- yield break;
+ return [ParameterInstances.Empty];
}
+ var result = new List();
foreach (var argumentsAttribute in benchmark.GetCustomAttributes())
{
if (parameterDefinitions.Length != argumentsAttribute.Values.Length)
throw new InvalidOperationException($"Benchmark {benchmark.Name} has invalid number of defined arguments provided with [Arguments]! {argumentsAttribute.Values.Length} instead of {parameterDefinitions.Length}.");
- yield return new ParameterInstances(
- argumentsAttribute
- .Values
+ result.Add(
+ new(argumentsAttribute.Values
.Select((value, index) =>
- {
- var definition = parameterDefinitions[index];
- var type = definition.ParameterType;
- return new ParameterInstance(definition, Map(value, type), summaryStyle);
- })
- .ToArray());
+ {
+ var definition = parameterDefinitions[index];
+ return new ParameterInstance(definition, new ParameterValue.Constant(value, definition.ParameterType), summaryStyle);
+ })
+ .ToArray()
+ )
+ );
}
if (!benchmark.HasAttribute())
- yield break;
+ return result;
var argumentsSourceAttribute = benchmark.GetCustomAttribute()!;
var targetType = argumentsSourceAttribute.Type ?? benchmarkType;
- var valuesInfo = GetValidValuesForParamsSource(targetType, argumentsSourceAttribute.Name);
+ var valuesInfo = await GetValidValuesForParamsSourceAsync(targetType, argumentsSourceAttribute.Name, cancellationToken).ConfigureAwait();
for (int sourceIndex = 0; sourceIndex < valuesInfo.values.Length; sourceIndex++)
- yield return SmartParamBuilder.CreateForArguments(benchmark, parameterDefinitions, valuesInfo, sourceIndex, summaryStyle);
+ result.Add(SmartParamBuilder.CreateForArguments(benchmark, parameterDefinitions, valuesInfo, sourceIndex, summaryStyle));
+
+ return result;
}
- private static ImmutableArray GetFilteredBenchmarks(IEnumerable benchmarks, IEnumerable filters)
- => benchmarks.Where(benchmark => filters.All(filter => filter.Predicate(benchmark))).ToImmutableArray();
+ private static ImmutableArray GetFilteredBenchmarks(BenchmarkCase[] benchmarks, IEnumerable filters)
+ => [.. benchmarks.Where(benchmark => filters.All(filter => filter.Predicate(benchmark)))];
private static void AssertMethodHasCorrectSignature(string methodType, MethodInfo methodInfo)
{
@@ -277,57 +304,93 @@ private static void AssertMethodIsNotGeneric(string methodType, MethodInfo metho
throw new InvalidBenchmarkDeclarationException($"{methodType} method {methodInfo.Name} is generic.\nGeneric {methodType} methods are not supported.");
}
- private static object?[] GetValidValues(object?[] values, Type parameterType)
- => values.Select(value => Map(value, parameterType)).ToArray();
+ private static IReadOnlyList GetValidValues(object?[] values, Type parameterType)
+ => [.. values.Select(value => new ParameterValue.Constant(value, parameterType))];
- private static object? Map(object? providedValue, Type type)
+ private static async ValueTask<(MemberInfo source, object?[] values)> GetValidValuesForParamsSourceAsync(Type sourceType, string sourceName, CancellationToken cancellationToken)
{
- if (providedValue == null)
- return null;
+ var source = sourceType.FindSourceMember(sourceName);
- if (providedValue.GetType().IsArray)
- {
- return ArrayParam.FromObject(providedValue);
- }
- // Usually providedValue contains all needed type information,
- // but in case of F# enum types in attributes are erased.
- // We can to restore them from types of arguments and fields.
- // See also:
- // https://github.com/dotnet/fsharp/issues/995
- else if (providedValue.GetType().IsEnum || type.IsEnum)
- {
- return EnumParam.FromObject(providedValue, type);
- }
- return providedValue;
+ if (source == null)
+ throw NoSourceMemberFound(sourceType, sourceName);
+
+ // A source method may have parameters as long as they are all optional (e.g. an async iterator with an
+ // [EnumeratorCancellation] CancellationToken); we invoke it with their default values.
+ object? sourceValue = source is MethodInfo method
+ ? method.Invoke(method.IsStatic ? null : Activator.CreateInstance(sourceType), GetDefaultArguments(method))
+ : ((PropertyInfo) source).GetValue(((PropertyInfo) source).GetMethod!.IsStatic ? null : Activator.CreateInstance(sourceType)!);
+
+ return (source, await ToArrayAsync(sourceValue, source, sourceType, cancellationToken).ConfigureAwait());
}
- private static (MemberInfo source, object[] values) GetValidValuesForParamsSource(Type sourceType, string sourceName)
+ private static InvalidBenchmarkDeclarationException NoSourceMemberFound(Type sourceType, string sourceName)
{
- var paramsSourceMethod = sourceType.GetAllMethods().FirstOrDefault(method => method.Name == sourceName && method.IsPublic);
+ var namedMethods = sourceType.GetAllMethods().Where(method => method.Name == sourceName && method.IsPublic).ToArray();
- if (paramsSourceMethod != default)
- return (paramsSourceMethod, ToArray(
- paramsSourceMethod.Invoke(paramsSourceMethod.IsStatic ? null : Activator.CreateInstance(sourceType), null)!,
- paramsSourceMethod,
- sourceType));
+ if (namedMethods.Any(method => method.IsGenericMethodDefinition))
+ return new InvalidBenchmarkDeclarationException($"Source method {sourceName} of type {sourceType.GetDisplayName()} is generic.\nGeneric source methods are not supported.");
- var paramsSourceProperty = sourceType.GetAllProperties().FirstOrDefault(property => property.Name == sourceName && property.GetMethod?.IsPublic == true);
+ return namedMethods.Length > 0
+ ? new InvalidBenchmarkDeclarationException($"{sourceType.Name}.{sourceName} has required parameters, unable to read values for [ParamsSource]/[ArgumentsSource]. A source method must be parameterless or have only optional parameters.")
+ : new InvalidBenchmarkDeclarationException($"{sourceType.Name} has no public, accessible method/property called {sourceName}, unable to read values for [ParamsSource].");
+ }
- if (paramsSourceProperty == null)
- throw new InvalidBenchmarkDeclarationException($"{sourceType.Name} has no public, accessible method/property called {sourceName}, unable to read values for [ParamsSource]");
+ // Default argument values for an all-optional-parameter source method. A parameter can be optional without declaring
+ // a default ([Optional] with no [DefaultParameterValue]), and MethodInfo.Invoke(object, object[]) does no optional-parameter
+ // binding - so we pass default(T), which is what the C# compiler passes at a call site that omits the argument.
+ private static object?[]? GetDefaultArguments(MethodInfo method)
+ {
+ var parameters = method.GetParameters();
+ if (parameters.Length == 0)
+ return null;
- return (paramsSourceProperty, ToArray(
- paramsSourceProperty.GetValue(paramsSourceProperty.GetMethod!.IsStatic ? null : Activator.CreateInstance(sourceType)!)!,
- paramsSourceProperty,
- sourceType));
+ var arguments = new object?[parameters.Length];
+ for (int i = 0; i < parameters.Length; i++)
+ arguments[i] = parameters[i].GetDefaultArgumentValue();
+ return arguments;
}
- private static object[] ToArray(object sourceValue, MemberInfo memberInfo, Type type)
+ private static async ValueTask ToArrayAsync(object? sourceValue, MemberInfo memberInfo, Type type, CancellationToken cancellationToken)
{
- if (!(sourceValue is IEnumerable collection))
- throw new InvalidBenchmarkDeclarationException($"{memberInfo.Name} of type {type.Name} does not implement IEnumerable, unable to read values for [ParamsSource]");
+ var sourceType = memberInfo is MethodInfo methodInfo
+ ? methodInfo.ReturnType
+ : ((PropertyInfo) memberInfo).PropertyType;
+
+ // Checked before the shape, so a null async source reports the same declaration error a null
+ // synchronous source does instead of failing while being enumerated.
+ if (sourceValue == null)
+ throw new InvalidBenchmarkDeclarationException($"{memberInfo.Name} of type {type.Name} returned null, unable to read values for [ParamsSource]/[ArgumentsSource].");
+
+ // Reading the values puts each into an object[], which a ref struct cannot enter - the enumeration fails
+ // inside reflection saying nothing about the benchmark. Expressible since .NET 10 gave IEnumerable an
+ // allows-ref-struct type parameter. Asked of both shapes, and so ahead of either: an async source reads
+ // its values into the same object[]. SourceReturnTypeValidator reports the declaration this substitutes.
+ if (memberInfo.GetSourceReturnType().TryGetSourceElementType(out var refLikeCandidate) && refLikeCandidate.IsByRefLike())
+ throw new InvalidBenchmarkDeclarationException(
+ $"{type.Name}.{memberInfo.Name} yields {refLikeCandidate.GetDisplayName()}, which is a ref struct, and BenchmarkDotNet cannot read a value into one."
+ + " Please, yield what the value is built from - IEnumerable for a ReadOnlySpan parameter - and let the benchmark take the ref struct.");
+
+ // Only IAsyncEnumerable is supported for async sources (not the await-foreach pattern). Decided from
+ // the declared type, and before the synchronous check, because that is what the generated code binds:
+ // an async-declared source whose value also implements IEnumerable must not be read synchronously here.
+ if (sourceType.IsIAsyncEnumerable(out var elementType))
+ {
+ List items = [];
+#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
+ await foreach (var item in DynamicAwaitHelper.EnumerateSourceAsync(sourceValue, elementType).ConfigureAwait(cancellationToken))
+#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
+ {
+ items.Add(item);
+ }
+ return [.. items];
+ }
+
+ // Synchronous sources are matched on the value: the declared type is often looser than what is returned
+ // (e.g. a non-generic IEnumerable), and the generated code binds the IEnumerable overload either way.
+ if (sourceValue is IEnumerable collection)
+ return [.. collection];
- return collection.Cast().ToArray();
+ throw new InvalidBenchmarkDeclarationException($"{memberInfo.Name} of type {type.Name} does not implement IEnumerable or IAsyncEnumerable, unable to read values for [ParamsSource]");
}
private static object?[] GetAllValidValues(Type parameterType)
@@ -338,16 +401,16 @@ private static object[] ToArray(object sourceValue, MemberInfo memberInfo, Type
if (parameterType.GetTypeInfo().IsEnum)
{
if (parameterType.GetTypeInfo().IsDefined(typeof(FlagsAttribute)))
- return [Activator.CreateInstance(parameterType)!];
+ return [Activator.CreateInstance(parameterType)];
- return Enum.GetValues(parameterType).Cast().ToArray();
+ return [.. Enum.GetValues(parameterType).Cast()];
}
var nullableUnderlyingType = Nullable.GetUnderlyingType(parameterType);
if (nullableUnderlyingType != null)
- return new object?[] { null }.Concat(GetAllValidValues(nullableUnderlyingType)).ToArray();
+ return [null, .. GetAllValidValues(nullableUnderlyingType)];
- return [Activator.CreateInstance(parameterType)!];
+ return [Activator.CreateInstance(parameterType)];
}
}
}
diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs b/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs
index 359d123801..29f531ade2 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs
@@ -1,19 +1,21 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Helpers;
namespace BenchmarkDotNet.Running
{
- public class BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConfig config, bool containsBenchmarkDeclarations, CompositeInProcessDiagnoser compositeInProcessDiagnoser) : IDisposable
+ public class BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConfig config, bool containsBenchmarkDeclarations, CompositeInProcessDiagnoser compositeInProcessDiagnoser) : IDisposable, IAsyncDisposable
{
public BenchmarkRunInfo(BenchmarkCase[] benchmarksCases, Type type, ImmutableConfig config, CompositeInProcessDiagnoser compositeInProcessDiagnoser)
: this(benchmarksCases, type, config, benchmarksCases.Length > 0, compositeInProcessDiagnoser) { }
+ public ValueTask DisposeAsync() => BenchmarksCases.DisposeAllAsync();
+
public void Dispose()
{
- foreach (var benchmarkCase in BenchmarksCases)
- {
- benchmarkCase.Dispose();
- }
+ using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
+ context.ExecuteUntilComplete(DisposeAsync());
}
public BenchmarkCase[] BenchmarksCases { get; } = benchmarksCase;
diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
index 056e5f2e4e..775b02bfe5 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs
@@ -82,7 +82,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu
var (supportedBenchmarks, validationErrors) = await GetSupportedBenchmarks(benchmarkRunInfos, resolver).ConfigureAwait();
- validationErrors.AddRange(await Validate(supportedBenchmarks).ConfigureAwait());
+ validationErrors.AddRange(await Validate(supportedBenchmarks, cancellationToken).ConfigureAwait());
foreach (var validationError in validationErrors)
eventProcessor.OnValidationError(validationError);
@@ -122,10 +122,10 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
await foreach (var (buildPartition, buildResult) in
BuildSequential(compositeLogger, rootArtifactsFolderPath, sequentialBuildPartitions, globalChronometer, eventProcessor, cancellationToken).ConfigureAwait())
+#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
{
buildResults.Add(buildPartition, buildResult);
}
-#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
}
var allBuildsHaveFailed = buildResults.Values.All(buildResult => !buildResult.IsBuildSuccess);
@@ -197,10 +197,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu
// some benchmarks might be using parameters that have locking finalizers
// so we need to dispose them after we are done running the benchmarks
// see https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more
- foreach (var benchmarkInfo in benchmarkRunInfos)
- {
- benchmarkInfo.Dispose();
- }
+ await benchmarkRunInfos.DisposeAllAsync().ConfigureAwait();
compositeLogger.WriteLineHeader("// * Artifacts cleanup *");
Cleanup(compositeLogger, new HashSet(artifactsToCleanup.Distinct()));
@@ -332,7 +329,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu
logFilePath,
runEnd.GetTimeSpan() - runStart.GetTimeSpan(),
cultureInfo,
- [.. await Validate(benchmarkRunInfo).ConfigureAwait(false)], // validate them once again, but don't print the output
+ [.. await Validate([benchmarkRunInfo], cancellationToken).ConfigureAwait(false)], // validate them once again, but don't print the output
[.. config.GetColumnHidingRules()]
),
benchmarksToRunCount
@@ -402,11 +399,24 @@ private static async ValueTask PrintSummary(ILogger logger, ImmutableConfig conf
logger.WriteLineHeader("// ***** BenchmarkRunner: End *****");
}
- private static async ValueTask> Validate(params BenchmarkRunInfo[] benchmarks)
- => await benchmarks
- .ToAsyncEnumerable()
- .SelectMany(benchmark => benchmark.Config.GetCompositeValidator().ValidateAsync(new ValidationParameters(benchmark.BenchmarksCases, benchmark.Config)))
- .ToArrayAsync().ConfigureAwait(false);
+ // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why.
+ private static async ValueTask> Validate(BenchmarkRunInfo[] benchmarks, CancellationToken cancellationToken)
+ {
+ var errors = new List();
+
+ foreach (var benchmark in benchmarks)
+ {
+ var validationParameters = new ValidationParameters(benchmark.BenchmarksCases, benchmark.Config);
+#pragma warning disable CA2007
+ await foreach (var error in benchmark.Config.GetCompositeValidator().ValidateAsync(validationParameters).ConfigureAwait(cancellationToken))
+#pragma warning restore CA2007
+ {
+ errors.Add(error);
+ }
+ }
+
+ return errors;
+ }
private static async ValueTask> BuildInParallel(
ILogger logger,
@@ -720,26 +730,29 @@ private static void LogTotalTime(ILogger logger, TimeSpan time, int executedBenc
continue;
}
- var validBenchmarks = await benchmarkRunInfo.BenchmarksCases
- .ToAsyncEnumerable()
- .Where(async (benchmark, _) =>
+ // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why.
+ var validBenchmarks = new List();
+ foreach (var benchmark in benchmarkRunInfo.BenchmarksCases)
+ {
+ var errors = new List();
+#pragma warning disable CA2007
+ await foreach (var error in benchmark.GetToolchain().ValidateAsync(benchmark, resolver).ConfigureAwait())
+#pragma warning restore CA2007
{
+ errors.Add(error);
+ }
- var errors = await benchmark.GetToolchain()
- .ValidateAsync(benchmark, resolver)
- .ToArrayAsync()
- .ConfigureAwait();
-
- validationErrors.AddRange(errors);
+ validationErrors.AddRange(errors);
- return !errors.Any(error => error.IsCritical);
- })
- .ToArrayAsync()
- .ConfigureAwait();
+ if (!errors.Any(error => error.IsCritical))
+ {
+ validBenchmarks.Add(benchmark);
+ }
+ }
runInfos.Add(
new BenchmarkRunInfo(
- validBenchmarks,
+ validBenchmarks.ToArray(),
benchmarkRunInfo.Type,
benchmarkRunInfo.Config,
benchmarkRunInfo.CompositeInProcessDiagnoser
diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs
index 573930b2fc..0b55f5aaa0 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs
@@ -1,6 +1,7 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using JetBrains.Annotations;
@@ -150,7 +151,7 @@ public static async ValueTask RunAsync(BenchmarkRunInfo[] benchmarkRu
private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type type, IConfig? config, string[]? args, CancellationToken cancellationToken)
{
var summaries = args == null
- ? await BenchmarkRunnerClean.Run([BenchmarkConverter.TypeToBenchmarks(type, config)], cancellationToken).ConfigureAwait(false)
+ ? await BenchmarkRunnerClean.Run([await BenchmarkConverter.TypeToBenchmarksAsync(type, config, cancellationToken).ConfigureAwait()], cancellationToken).ConfigureAwait(false)
: await new BenchmarkSwitcher([type]).RunWithDirtyAssemblyResolveHelper(args, config, false, cancellationToken).ConfigureAwait(false);
return summaries.SingleOrDefault()
@@ -160,7 +161,7 @@ private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type t
[MethodImpl(MethodImplOptions.NoInlining)]
private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type type, MethodInfo[] methods, IConfig? config, CancellationToken cancellationToken)
{
- var summaries = await BenchmarkRunnerClean.Run([BenchmarkConverter.MethodsToBenchmarks(type, methods, config)], cancellationToken).ConfigureAwait(false);
+ var summaries = await BenchmarkRunnerClean.Run([await BenchmarkConverter.MethodsToBenchmarksAsync(type, methods, config, cancellationToken).ConfigureAwait()], cancellationToken).ConfigureAwait(false);
return summaries.SingleOrDefault()
?? Summary.ValidationFailed($"No benchmarks found in type '{type.Name}'", string.Empty, string.Empty);
@@ -169,15 +170,24 @@ private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type t
[MethodImpl(MethodImplOptions.NoInlining)]
private static async ValueTask RunWithDirtyAssemblyResolveHelper(Assembly assembly, IConfig? config, string[]? args, CancellationToken cancellationToken)
=> args == null
- ? await BenchmarkRunnerClean.Run(assembly.GetRunnableBenchmarks().Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray(), cancellationToken).ConfigureAwait(false)
+ ? await BenchmarkRunnerClean.Run(await TypesToBenchmarksAsync(assembly.GetRunnableBenchmarks(), config, cancellationToken).ConfigureAwait(), cancellationToken).ConfigureAwait(false)
: (await new BenchmarkSwitcher(assembly).RunWithDirtyAssemblyResolveHelper(args, config, false, cancellationToken).ConfigureAwait(false)).ToArray();
[MethodImpl(MethodImplOptions.NoInlining)]
private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type[] types, IConfig? config, string[]? args, CancellationToken cancellationToken)
=> args == null
- ? await BenchmarkRunnerClean.Run(types.Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray(), cancellationToken).ConfigureAwait(false)
+ ? await BenchmarkRunnerClean.Run(await TypesToBenchmarksAsync(types, config, cancellationToken).ConfigureAwait(), cancellationToken).ConfigureAwait(false)
: (await new BenchmarkSwitcher(types).RunWithDirtyAssemblyResolveHelper(args, config, false, cancellationToken).ConfigureAwait(false)).ToArray();
+ // Convert types sequentially (not concurrently) because reading params/arguments sources runs user code.
+ private static async ValueTask TypesToBenchmarksAsync(IEnumerable types, IConfig? config, CancellationToken cancellationToken)
+ {
+ var runInfos = new List();
+ foreach (var type in types)
+ runInfos.Add(await BenchmarkConverter.TypeToBenchmarksAsync(type, config, cancellationToken).ConfigureAwait());
+ return runInfos.ToArray();
+ }
+
[MethodImpl(MethodImplOptions.NoInlining)]
private static ValueTask RunWithDirtyAssemblyResolveHelper(BenchmarkRunInfo[] benchmarkRunInfos, CancellationToken cancellationToken)
=> BenchmarkRunnerClean.Run(benchmarkRunInfos, cancellationToken);
diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs
index d789b8694a..b169af6d52 100644
--- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs
+++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs
@@ -4,6 +4,7 @@
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Extensions;
+using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Parameters;
@@ -148,7 +149,7 @@ internal async ValueTask> RunWithDirtyAssemblyResolveHelper
if (options.ListBenchmarkCaseMode != ListBenchmarkCaseMode.Disabled)
{
- PrintList(logger, effectiveConfig, allAvailableTypesWithRunnableBenchmarks, options);
+ await PrintListAsync(logger, effectiveConfig, allAvailableTypesWithRunnableBenchmarks, options, cancellationToken).ConfigureAwait();
return [];
}
@@ -161,23 +162,22 @@ internal async ValueTask> RunWithDirtyAssemblyResolveHelper
return await ApplesToApples(ImmutableConfigBuilder.Create(effectiveConfig), benchmarksToFilter, logger, options, cancellationToken).ConfigureAwait(false);
}
- var filteredBenchmarks = TypeFilter.Filter(effectiveConfig, benchmarksToFilter);
+ var filteredBenchmarks = await TypeFilter.FilterAsync(effectiveConfig, benchmarksToFilter, cancellationToken).ConfigureAwait();
if (filteredBenchmarks.IsEmpty())
{
- userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, [.. options.Filters]);
+ await userInteraction.PrintWrongFilterInfoAsync(benchmarksToFilter, logger, [.. options.Filters], cancellationToken).ConfigureAwait();
return [];
}
return await BenchmarkRunnerClean.Run(filteredBenchmarks, cancellationToken).ConfigureAwait(false);
}
- private static void PrintList(ILogger nonNullLogger, IConfig effectiveConfig, IReadOnlyList allAvailableTypesWithRunnableBenchmarks, CommandLineOptions options)
+ private static async ValueTask PrintListAsync(ILogger nonNullLogger, IConfig effectiveConfig, IReadOnlyList allAvailableTypesWithRunnableBenchmarks, CommandLineOptions options, CancellationToken cancellationToken)
{
var printer = new BenchmarkCasesPrinter(options.ListBenchmarkCaseMode);
- var benchmarkCases = TypeFilter
- .Filter(effectiveConfig, allAvailableTypesWithRunnableBenchmarks)
+ var benchmarkCases = (await TypeFilter.FilterAsync(effectiveConfig, allAvailableTypesWithRunnableBenchmarks, cancellationToken).ConfigureAwait())
.SelectMany(p => p.BenchmarksCases);
printer.Print(benchmarkCases, nonNullLogger);
@@ -213,10 +213,10 @@ private async ValueTask> ApplesToApples(ImmutableConfig eff
invocationCountConfig.RemoveAllDiagnosers();
invocationCountConfig.AddJob(invocationCountJob);
- var invocationCountBenchmarks = TypeFilter.Filter(invocationCountConfig, benchmarksToFilter);
+ var invocationCountBenchmarks = await TypeFilter.FilterAsync(invocationCountConfig, benchmarksToFilter, cancellationToken).ConfigureAwait();
if (invocationCountBenchmarks.IsEmpty())
{
- userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, [.. options.Filters]);
+ await userInteraction.PrintWrongFilterInfoAsync(benchmarksToFilter, logger, [.. options.Filters], cancellationToken).ConfigureAwait();
return [];
}
@@ -230,7 +230,7 @@ private async ValueTask> ApplesToApples(ImmutableConfig eff
report => report.AllMeasurements.Single(measurement => measurement.IsWorkload() && measurement.IterationStage == Engines.IterationStage.Actual));
int iterationCount = baselineJob.Run.IterationCount;
- BenchmarkRunInfo[] benchmarksWithoutInvocationCount = TypeFilter.Filter(effectiveConfig, benchmarksToFilter);
+ BenchmarkRunInfo[] benchmarksWithoutInvocationCount = await TypeFilter.FilterAsync(effectiveConfig, benchmarksToFilter, cancellationToken).ConfigureAwait();
BenchmarkRunInfo[] benchmarksWithInvocationCount = benchmarksWithoutInvocationCount
.Select(benchmarkInfo => new BenchmarkRunInfo(
benchmarkInfo.BenchmarksCases.Select(benchmark =>
diff --git a/src/BenchmarkDotNet/Running/IUserInteraction.cs b/src/BenchmarkDotNet/Running/IUserInteraction.cs
index e697f294e2..0c68d39269 100644
--- a/src/BenchmarkDotNet/Running/IUserInteraction.cs
+++ b/src/BenchmarkDotNet/Running/IUserInteraction.cs
@@ -6,7 +6,7 @@ internal interface IUserInteraction
{
void PrintNoBenchmarksError(ILogger logger);
- void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, string[] userFilters);
+ ValueTask PrintWrongFilterInfoAsync(IReadOnlyList allTypes, ILogger logger, string[] userFilters, CancellationToken cancellationToken);
IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger);
}
diff --git a/src/BenchmarkDotNet/Running/TypeFilter.cs b/src/BenchmarkDotNet/Running/TypeFilter.cs
index b9420915b5..25846ee252 100644
--- a/src/BenchmarkDotNet/Running/TypeFilter.cs
+++ b/src/BenchmarkDotNet/Running/TypeFilter.cs
@@ -1,4 +1,5 @@
using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Loggers;
@@ -61,9 +62,21 @@ public static (bool allTypesValid, IReadOnlyList runnable) GetTypesWithRun
}
public static BenchmarkRunInfo[] Filter(IConfig effectiveConfig, IEnumerable types)
- => types
- .Select(type => BenchmarkConverter.TypeToBenchmarks(type, effectiveConfig))
- .Where(info => info.BenchmarksCases.Any())
- .ToArray();
+ {
+ using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent();
+ return context.ExecuteUntilComplete(FilterAsync(effectiveConfig, types, CancellationToken.None));
+ }
+
+ public static async ValueTask FilterAsync(IConfig effectiveConfig, IEnumerable types, CancellationToken cancellationToken)
+ {
+ var result = new List();
+ foreach (var type in types)
+ {
+ var info = await BenchmarkConverter.TypeToBenchmarksAsync(type, effectiveConfig, cancellationToken).ConfigureAwait();
+ if (info.BenchmarksCases.Any())
+ result.Add(info);
+ }
+ return result.ToArray();
+ }
}
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Running/UserInteraction.cs b/src/BenchmarkDotNet/Running/UserInteraction.cs
index 86defdc5b4..fa4412e0df 100644
--- a/src/BenchmarkDotNet/Running/UserInteraction.cs
+++ b/src/BenchmarkDotNet/Running/UserInteraction.cs
@@ -50,9 +50,9 @@ public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger)
return selectedTypes;
}
- public void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, string[] userFilters)
+ public async ValueTask PrintWrongFilterInfoAsync(IReadOnlyList allTypes, ILogger logger, string[] userFilters, CancellationToken cancellationToken)
{
- var correctionSuggester = new CorrectionsSuggester(allTypes);
+ var correctionSuggester = await CorrectionsSuggester.CreateAsync(allTypes, cancellationToken).ConfigureAwait();
var filterToNames = userFilters
.Select(userFilter => (userFilter: userFilter, suggestedBenchmarkNames: correctionSuggester.SuggestFor(userFilter)))
diff --git a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
index 47029d33ac..1c457f8a7b 100644
--- a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
+++ b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt
@@ -1,20 +1,13 @@
//
// this file must not be importing any namespaces
-// we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778
+// we should use full names everywhere to avoid any potential naming conflicts, example: #778, #1007, #2821
#if !NET7_0_OR_GREATER
-// User code could poly-fill types and leave them public, so we disable the warning just in case.
+// When the benchmark type declares `required` members the runnable inherits them and the compiler emits
+// CompilerFeatureRequiredAttribute on it. That attribute doesn't exist before net7.0, so we poly-fill it. User code
+// could poly-fill it too and leave it public, so we disable the conflict warning just in case.
#pragma warning disable CS0436 // Type conflicts with imported type
-namespace System.Diagnostics.CodeAnalysis
-{
- [global::System.AttributeUsage(global::System.AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- internal sealed class SetsRequiredMembersAttribute : global::System.Attribute
- {
- }
-}
-
namespace System.Runtime.CompilerServices
{
[global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)]
@@ -31,12 +24,6 @@ namespace System.Runtime.CompilerServices
public const global::System.String RefStructs = nameof(RefStructs);
public const global::System.String RequiredMembers = nameof(RequiredMembers);
}
-
- [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct | global::System.AttributeTargets.Field | global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- internal sealed class RequiredMemberAttribute : global::System.Attribute
- {
- }
}
#endif
diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt
index ac9d89c84c..961c40cce3 100644
--- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt
+++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt
@@ -2,16 +2,26 @@
[global::BenchmarkDotNet.Attributes.CompilerServices.AggressivelyOptimizeMethods]
public sealed class Runnable_$ID$ : $WorkloadTypeName$
{
- public static async global::System.Threading.Tasks.ValueTask Run(global::BenchmarkDotNet.Engines.IHost host, global::System.String benchmarkName, global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode)
+ public static async global::System.Threading.Tasks.ValueTask __Run(global::BenchmarkDotNet.Engines.IHost host, global::System.String benchmarkName, global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode)
{
- global::BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance = new global::BenchmarkDotNet.Autogenerated.Runnable_$ID$(host.CancellationToken);
+ global::System.Threading.CancellationToken cancellationToken = host.CancellationToken;
+ $CancellationTokenAssignment$
+ // The constructor captures each instance [ParamsSource]/[ArgumentsSource] enumerable via an out parameter;
+ // the object initializer then assigns the values (init-/required-capable), while static params and argument fields are assigned afterwards.
+ // We cannot use [SetsRequiredMembers] and assign them in the ctor, because a source may be async.
+ global::BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance = new global::BenchmarkDotNet.Autogenerated.Runnable_$ID$($SourceOutArguments$)
+ {
+ $CancellationTokenInitializer$
+ $ParamsInitializer$
+ };
+ $StaticParamsAndArgsContent$
host.WriteLine();
foreach (global::System.String infoLine in global::BenchmarkDotNet.Environments.BenchmarkEnvironmentInfo.GetCurrent().ToFormattedString())
{
host.WriteLine($"// {infoLine}");
}
- global::BenchmarkDotNet.Jobs.Job job = new global::BenchmarkDotNet.Jobs.Job(); // use full name to avoid naming conflicts, #778
+ global::BenchmarkDotNet.Jobs.Job job = new global::BenchmarkDotNet.Jobs.Job();
$JobSetDefinition$;
job.Freeze();
host.WriteLine($"// Job: {job.DisplayInfo}");
@@ -36,47 +46,11 @@
}
await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.BeforeEngine, host.CancellationToken));
- global::BenchmarkDotNet.Engines.EngineParameters engineParameters = new global::BenchmarkDotNet.Engines.EngineParameters()
- {
- Host = host,
- WorkloadMethods = instance.__ResolveWorkloadMethods(host),
- WorkloadActionUnroll = instance.WorkloadActionUnroll,
- WorkloadActionNoUnroll = instance.WorkloadActionNoUnroll,
- OverheadActionNoUnroll = instance.OverheadActionNoUnroll,
- OverheadActionUnroll = instance.OverheadActionUnroll,
- GlobalSetupAction = instance.__GlobalSetup,
- GlobalCleanupAction = instance.__GlobalCleanup,
- IterationSetupAction = instance.__IterationSetup,
- IterationCleanupAction = instance.__IterationCleanup,
- TargetJob = job,
- OperationsPerInvoke = $OperationsPerInvoke$,
- RunExtraIteration = $RunExtraIteration$,
- BenchmarkName = benchmarkName,
- InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler
- };
-
- global::BenchmarkDotNet.Engines.RunResults results = await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(new $EngineFactoryType$().Create(engineParameters).RunAsync());
- host.ReportResults(results); // printing costs memory, do this after runs
-
- instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;)
- await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.AfterEngine, host.CancellationToken).ConfigureAwait(false);
- }
-
- [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
- public Runnable_$ID$(global::System.Threading.CancellationToken cancellationToken)
- {
- $InitializeArgumentFields$
- $ParamsContent$
- $CancellationTokenAssignment$
- }
-
- $DeclareFieldsContainer$
-
- private global::System.Reflection.MethodInfo[] __ResolveWorkloadMethods(global::BenchmarkDotNet.Engines.IHost host)
- {
- // Best-effort: the jit stage uses the resolved method(s) to watch their JIT tier-up events, and falls back
- // to a fixed delay when none are resolved. So neither a missed match nor a reflection failure (e.g. a
- // same-named overload's parameter type fails to load) may break the benchmark — report and return empty.
+ // Resolve the benchmark method(s) for the jit stage to watch their JIT tier-up events. Best-effort: it
+ // falls back to a fixed delay when none are resolved, so neither a missed match nor a reflection failure
+ // (e.g. a same-named overload's parameter type fails to load) may break the benchmark. Inlined here rather
+ // than a helper method so it can't collide with a benchmark member's name.
+ global::System.Reflection.MethodInfo[] workloadMethods = global::System.Array.Empty();
try
{
global::System.Type[] parameterTypes = $WorkloadMethodParameterTypes$;
@@ -104,17 +78,49 @@
}
if (isMatch)
{
- return new global::System.Reflection.MethodInfo[] { candidate };
+ workloadMethods = new global::System.Reflection.MethodInfo[] { candidate };
+ break;
}
}
+ if (workloadMethods.Length == 0)
+ {
+ host.WriteLine("// Could not resolve the benchmark method '$WorkloadMethodName$' to watch JIT tier-up events; the jit stage will fall back to a fixed delay.");
+ }
}
catch (global::System.Exception e)
{
- host.SendError($"Exception during __ResolveWorkloadMethods!{(global::System.Environment.NewLine)}{e}");
- return global::System.Array.Empty();
+ host.SendError($"Exception during workload method resolution!{(global::System.Environment.NewLine)}{e}");
}
- host.WriteLine("// Could not resolve the benchmark method '$WorkloadMethodName$' to watch JIT tier-up events; the jit stage will fall back to a fixed delay.");
- return global::System.Array.Empty();
+
+ global::BenchmarkDotNet.Engines.EngineParameters engineParameters = new global::BenchmarkDotNet.Engines.EngineParameters()
+ {
+ Host = host,
+ WorkloadMethods = workloadMethods,
+ WorkloadActionUnroll = instance.__WorkloadActionUnroll,
+ WorkloadActionNoUnroll = instance.__WorkloadActionNoUnroll,
+ OverheadActionNoUnroll = instance.__OverheadActionNoUnroll,
+ OverheadActionUnroll = instance.__OverheadActionUnroll,
+ GlobalSetupAction = instance.__GlobalSetup,
+ GlobalCleanupAction = instance.__GlobalCleanup,
+ IterationSetupAction = instance.__IterationSetup,
+ IterationCleanupAction = instance.__IterationCleanup,
+ TargetJob = job,
+ OperationsPerInvoke = $OperationsPerInvoke$,
+ RunExtraIteration = $RunExtraIteration$,
+ BenchmarkName = benchmarkName,
+ InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler
+ };
+
+ global::BenchmarkDotNet.Engines.RunResults results = await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(new $EngineFactoryType$().Create(engineParameters).RunAsync());
+ host.ReportResults(results); // printing costs memory, do this after runs
+
+ instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;)
+ await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.AfterEngine, host.CancellationToken).ConfigureAwait(false);
+ }
+
+ private Runnable_$ID$($SourceOutParameters$)
+ {
+ $SourceCaptures$
}
private $GlobalSetupModifiers$ global::System.Threading.Tasks.ValueTask __GlobalSetup()
@@ -163,4 +169,6 @@
}
$CoreImpl$
+
+ $DeclareFieldsContainer$
}
\ No newline at end of file
diff --git a/src/BenchmarkDotNet/Templates/CsProj.txt b/src/BenchmarkDotNet/Templates/CsProj.txt
index 9afef8cf5e..b022015923 100644
--- a/src/BenchmarkDotNet/Templates/CsProj.txt
+++ b/src/BenchmarkDotNet/Templates/CsProj.txt
@@ -31,7 +31,7 @@
- latest
+ latest
$RUNTIMESETTINGS$
diff --git a/src/BenchmarkDotNet/Templates/R2RCsProj.txt b/src/BenchmarkDotNet/Templates/R2RCsProj.txt
index e881418b1b..238ecdb807 100644
--- a/src/BenchmarkDotNet/Templates/R2RCsProj.txt
+++ b/src/BenchmarkDotNet/Templates/R2RCsProj.txt
@@ -46,7 +46,7 @@
- latest
+ latest
diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt
index 53cc9bd36d..cf17cd2834 100644
--- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt
+++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt
@@ -47,7 +47,7 @@ $CORECLR_OVERRIDES$
- latest
+ latest
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs
index 13299866d7..4aa179be8b 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs
@@ -6,7 +6,7 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
-using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants;
+using static BenchmarkDotNet.Code.RunnableConstants;
namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation;
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs
index 030ea9cf3a..5b216bf10b 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs
@@ -7,7 +7,7 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
-using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants;
+using static BenchmarkDotNet.Code.RunnableConstants;
namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation;
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs
index 1f147e6f15..bb14b59d7a 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs
@@ -6,7 +6,7 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
-using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants;
+using static BenchmarkDotNet.Code.RunnableConstants;
namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation;
diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs
index 7646bddf84..cea0b38edc 100644
--- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs
+++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs
@@ -3,29 +3,12 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
-using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants;
+using static BenchmarkDotNet.Code.RunnableConstants;
namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation;
partial class RunnableEmitter
{
- // Roslyn generates ordinals in declaration order of every member.
- // We don't necessarily emit members in the same order (or at all in the case of Runnable_#.Run), so we map it to the expected Roslyn ordinal.
- // This doesn't really matter for the runtime, but it helps with the NaiveRunnableEmitDiff tests.
- protected virtual IReadOnlyDictionary AsyncMethodToOrdinalMap { get; } = new Dictionary