Skip to content

Allow for Config per method, introduce OS and OSArchitecture filters #1097

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions samples/BenchmarkDotNet.Samples/IntroMemoryRandomization.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
using BenchmarkDotNet.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BenchmarkDotNet.Samples
{
Expand All @@ -23,6 +19,12 @@ public void Setup()
}

[Benchmark]
public void Array() => System.Array.Copy(_array, _destination, Size);
[MemoryRandomization(false)]
public void Array_RandomizationDisabled() => Array.Copy(_array, _destination, Size);

[Benchmark]
[MemoryRandomization(true)]
[MaxIterationCount(40)] // the benchmark becomes multimodal and need a lower limit of max iterations than the default
public void Array_RandomizationEnabled() => Array.Copy(_array, _destination, Size);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace BenchmarkDotNet.Attributes
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public abstract class FilterConfigBaseAttribute : Attribute, IConfigSource
{
// CLS-Compliant Code requires a constructor without an array in the argument list
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Linq;
using BenchmarkDotNet.Filters;
using JetBrains.Annotations;
using System.Runtime.InteropServices;

namespace BenchmarkDotNet.Attributes
{
[PublicAPI]
public class OperatingSystemsArchitectureFilterAttribute : FilterConfigBaseAttribute
{
// CLS-Compliant Code requires a constructor without an array in the argument list
public OperatingSystemsArchitectureFilterAttribute() { }

/// <param name="allowed">if set to true, the architectures are enabled, if set to false, disabled</param>
public OperatingSystemsArchitectureFilterAttribute(bool allowed, params Architecture[] architectures)
: base(new SimpleFilter(_ =>
{
return allowed
? architectures.Any(architecture => RuntimeInformation.OSArchitecture == architecture)
: architectures.All(architecture => RuntimeInformation.OSArchitecture != architecture);
}))
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Linq;
using BenchmarkDotNet.Filters;
using JetBrains.Annotations;
using System.Runtime.InteropServices;

namespace BenchmarkDotNet.Attributes
{
public enum OS : byte
{
Windows,
Linux,
macOS,
/// <summary>
/// WebAssembly
/// </summary>
Browser
}

[PublicAPI]
public class OperatingSystemsFilterAttribute : FilterConfigBaseAttribute
{
private static readonly OSPlatform browser = OSPlatform.Create("BROWSER");

// CLS-Compliant Code requires a constructor without an array in the argument list
public OperatingSystemsFilterAttribute() { }

/// <param name="allowed">if set to true, the OSes beloning to platforms are enabled, if set to false, disabled</param>
public OperatingSystemsFilterAttribute(bool allowed, params OS[] platforms)
: base(new SimpleFilter(_ =>
{
return allowed
? platforms.Any(platform => RuntimeInformation.IsOSPlatform(Map(platform)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could take the string of a OSPlatform instead of PlatformID? (basically the string that gets passed into https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.osplatform.create)

I don't think PlatformID is going to work.

From https://docs.microsoft.com/en-us/dotnet/api/system.platformid

MacOSX | 6 | The operating system is Macintosh. This value was returned by Silverlight. On .NET Core, its replacement is Unix.

From that, it sounds like you won't be able to distinguish between macOS and Linux on .NET Core using PlatformID.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eerhardt I am not sure about string. For example: let's say that I dont't want to run the benchmark on Ubuntu. Should I put Unix? Linux? or Ubuntu? or Ubuntu16.04?

But I agree that PlatformID is not perfect, I had to implement a mapping on my own.

Maybe I should just introduce a new enum?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I put Unix? Linux? or Ubuntu? or Ubuntu16.04?

Do you want that deep of control? Do you want to be able to say "All Unixes" vs. "just Ubuntu, but not RedHat or OpenSUSE"? If so, it seems like a perfect place to use RIDs...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want that deep of control?

No, I just wanted to show that without an enum, the user needs to guess the right string value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been two years since I created this PR, but now I really need tha for Randomization in the perf repo so I've updated it. I decided to include my own simple enum, just so users don't have to type OS names manually (an avoid mistakes for "OSX" != "macOS" etc)

: platforms.All(platform => !RuntimeInformation.IsOSPlatform(Map(platform)));
}))
{
}

// OSPlatform is a struct so it can not be used as attribute argument and this is why we use PlatformID enum
private static OSPlatform Map(OS platform)
{
switch (platform)
{
case OS.Windows:
return OSPlatform.Windows;
case OS.Linux:
return OSPlatform.Linux;
case OS.macOS:
return OSPlatform.OSX;
case OS.Browser:
return browser;
default:
throw new NotSupportedException($"Platform {platform} is not supported");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace BenchmarkDotNet.Attributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] // users must not be able to define given mutator attribute more than once per type
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] // users must not be able to define given mutator attribute more than once per type
public class JobMutatorConfigBaseAttribute : Attribute, IConfigSource
{
// CLS-Compliant Code requires a constructor which use only CLS-compliant types
Expand Down
92 changes: 46 additions & 46 deletions src/BenchmarkDotNet/Running/BenchmarkConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,78 +23,78 @@ public static BenchmarkRunInfo TypeToBenchmarks(Type type, IConfig config = null

// We should check all methods including private to notify users about private methods with the [Benchmark] attribute
var bindingFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
var benchmarkMethods = type.GetMethods(bindingFlags).Where(method => method.HasAttribute<BenchmarkAttribute>()).ToArray();

var fullConfig = GetFullConfig(type, config);
var allMethods = type.GetMethods(bindingFlags);
return MethodsToBenchmarksWithFullConfig(type, allMethods, fullConfig);
return MethodsToBenchmarksWithFullConfig(type, benchmarkMethods, config);
}

public static BenchmarkRunInfo MethodsToBenchmarks(Type containingType, MethodInfo[] benchmarkMethods, IConfig config = null)
{
var fullConfig = GetFullConfig(containingType, config);

return MethodsToBenchmarksWithFullConfig(containingType, benchmarkMethods, fullConfig);
}
=> MethodsToBenchmarksWithFullConfig(containingType, benchmarkMethods, config);

private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type containingType, MethodInfo[] benchmarkMethods, ImmutableConfig immutableConfig)
private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, MethodInfo[] benchmarkMethods, IConfig config)
{
if (immutableConfig == null)
throw new ArgumentNullException(nameof(immutableConfig));

var helperMethods = containingType.GetMethods(); // benchmarkMethods can be filtered, without Setups, look #564

var globalSetupMethods = GetAttributedMethods<GlobalSetupAttribute>(helperMethods, "GlobalSetup");
var globalCleanupMethods = GetAttributedMethods<GlobalCleanupAttribute>(helperMethods, "GlobalCleanup");
var iterationSetupMethods = GetAttributedMethods<IterationSetupAttribute>(helperMethods, "IterationSetup");
var iterationCleanupMethods = GetAttributedMethods<IterationCleanupAttribute>(helperMethods, "IterationCleanup");

var targetMethods = benchmarkMethods.Where(method => method.HasAttribute<BenchmarkAttribute>()).ToArray();
var allPublicMethods = type.GetMethods(); // benchmarkMethods can be filtered, without Setups, look #564
var configPerType = GetFullTypeConfig(type, config);

var parameterDefinitions = GetParameterDefinitions(containingType);
var parameterInstancesList = parameterDefinitions.Expand(immutableConfig.SummaryStyle);
var globalSetupMethods = GetAttributedMethods<GlobalSetupAttribute>(allPublicMethods, "GlobalSetup");
var globalCleanupMethods = GetAttributedMethods<GlobalCleanupAttribute>(allPublicMethods, "GlobalCleanup");
var iterationSetupMethods = GetAttributedMethods<IterationSetupAttribute>(allPublicMethods, "IterationSetup");
var iterationCleanupMethods = GetAttributedMethods<IterationCleanupAttribute>(allPublicMethods, "IterationCleanup");

var jobs = immutableConfig.GetJobs();
var targets = GetTargets(benchmarkMethods, type, globalSetupMethods, globalCleanupMethods, iterationSetupMethods, iterationCleanupMethods).ToArray();

var targets = GetTargets(targetMethods, containingType, globalSetupMethods, globalCleanupMethods, iterationSetupMethods, iterationCleanupMethods).ToArray();
var parameterDefinitions = GetParameterDefinitions(type);
var parameterInstancesList = parameterDefinitions.Expand(configPerType.SummaryStyle);

var benchmarks = new List<BenchmarkCase>();

foreach (var target in targets)
{
var argumentsDefinitions = GetArgumentsDefinitions(target.WorkloadMethod, target.Type, immutableConfig.SummaryStyle).ToArray();
var argumentsDefinitions = GetArgumentsDefinitions(target.WorkloadMethod, target.Type, configPerType.SummaryStyle).ToArray();

var parameterInstances =
(from parameterInstance in parameterInstancesList
from argumentDefinition in argumentsDefinitions
select new ParameterInstances(parameterInstance.Items.Concat(argumentDefinition.Items).ToArray())).ToArray();

benchmarks.AddRange(
from job in jobs
var configPerMethod = GetFullMethodConfig(target.WorkloadMethod, configPerType);

var benchmarksForTarget =
from job in configPerMethod.GetJobs()
from parameterInstance in parameterInstances
select BenchmarkCase.Create(target, job, parameterInstance, immutableConfig)
);
select BenchmarkCase.Create(target, job, parameterInstance, configPerMethod);

benchmarks.AddRange(GetFilteredBenchmarks(benchmarksForTarget, configPerMethod.GetFilters()));
}

var filters = immutableConfig.GetFilters().ToArray();
var filteredBenchmarks = GetFilteredBenchmarks(benchmarks, filters);
var orderedBenchmarks = immutableConfig.Orderer.GetExecutionOrder(filteredBenchmarks).ToArray();
var orderedBenchmarks = configPerType.Orderer.GetExecutionOrder(benchmarks.ToImmutableArray()).ToArray();

return new BenchmarkRunInfo(orderedBenchmarks, containingType, immutableConfig);
return new BenchmarkRunInfo(orderedBenchmarks, type, configPerType);
}

public static ImmutableConfig GetFullConfig(Type type, IConfig config)
private static ImmutableConfig GetFullTypeConfig(Type type, IConfig config)
{
config = config ?? DefaultConfig.Instance;
if (type != null)
{
var typeAttributes = type.GetTypeInfo().GetCustomAttributes(true).OfType<IConfigSource>();
var assemblyAttributes = type.GetTypeInfo().Assembly.GetCustomAttributes().OfType<IConfigSource>();
var allAttributes = typeAttributes.Concat(assemblyAttributes);
var configs = allAttributes.Select(attribute => attribute.Config)
.OrderBy(c => c.GetJobs().Count(job => job.Meta.IsMutator)); // configs with mutators must be the ones applied at the end

foreach (var configFromAttribute in configs)
config = ManualConfig.Union(config, configFromAttribute);
}

var typeAttributes = type.GetCustomAttributes(true).OfType<IConfigSource>();
var assemblyAttributes = type.Assembly.GetCustomAttributes().OfType<IConfigSource>();

foreach (var configFromAttribute in typeAttributes.Concat(assemblyAttributes))
config = ManualConfig.Union(config, configFromAttribute.Config);

return ImmutableConfigBuilder.Create(config);
}

private static ImmutableConfig GetFullMethodConfig(MethodInfo method, ImmutableConfig typeConfig)
{
var methodAttributes = method.GetCustomAttributes(true).OfType<IConfigSource>();

if (!methodAttributes.Any()) // the most common case
return typeConfig;

var config = ManualConfig.Create(typeConfig);
foreach (var configFromAttribute in methodAttributes)
config = ManualConfig.Union(config, configFromAttribute.Config);

return ImmutableConfigBuilder.Create(config);
}
Expand Down Expand Up @@ -251,7 +251,7 @@ private static string[] GetCategories(MethodInfo method)
return attributes.SelectMany(attr => attr.Categories).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}

private static ImmutableArray<BenchmarkCase> GetFilteredBenchmarks(IList<BenchmarkCase> benchmarks, IList<IFilter> filters)
private static ImmutableArray<BenchmarkCase> GetFilteredBenchmarks(IEnumerable<BenchmarkCase> benchmarks, IEnumerable<IFilter> filters)
=> benchmarks.Where(benchmark => filters.All(filter => filter.Predicate(benchmark))).ToImmutableArray();

private static void AssertMethodHasCorrectSignature(string methodType, MethodInfo methodInfo)
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class BenchmarkPartitioner
{
public static BuildPartition[] CreateForBuild(BenchmarkRunInfo[] supportedBenchmarks, IResolver resolver)
=> supportedBenchmarks
.SelectMany(info => info.BenchmarksCases.Select(benchmark => (benchmark, info.Config)))
.SelectMany(info => info.BenchmarksCases.Select(benchmark => (benchmark, benchmark.Config)))
.GroupBy(tuple => tuple.benchmark, BenchmarkRuntimePropertiesComparer.Instance)
.Select(group => new BuildPartition(group.Select((item, index) => new BenchmarkBuildInfo(item.benchmark, item.Config, index)).ToArray(), resolver))
.ToArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected Reports.Summary CanExecute(Type type, IConfig config = null, bool full
config = config.AddColumnProvider(DefaultColumnProviders.Instance);

// Make sure we ALWAYS combine the Config (default or passed in) with any Config applied to the Type/Class
var summary = BenchmarkRunner.Run(type, BenchmarkConverter.GetFullConfig(type, config));
var summary = BenchmarkRunner.Run(type, config);

if (fullValidation)
{
Expand Down
Loading