diff --git a/samples/BenchmarkDotNet.Samples/IntroMemoryRandomization.cs b/samples/BenchmarkDotNet.Samples/IntroMemoryRandomization.cs index 03d55574ec..5b9e417398 100644 --- a/samples/BenchmarkDotNet.Samples/IntroMemoryRandomization.cs +++ b/samples/BenchmarkDotNet.Samples/IntroMemoryRandomization.cs @@ -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 { @@ -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); } } diff --git a/src/BenchmarkDotNet/Attributes/Filters/FilterConfigBaseAttribute.cs b/src/BenchmarkDotNet/Attributes/Filters/FilterConfigBaseAttribute.cs index ac5584a012..2a1c2f142e 100644 --- a/src/BenchmarkDotNet/Attributes/Filters/FilterConfigBaseAttribute.cs +++ b/src/BenchmarkDotNet/Attributes/Filters/FilterConfigBaseAttribute.cs @@ -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 diff --git a/src/BenchmarkDotNet/Attributes/Filters/OperatingSystemsArchitectureFilterAttribute.cs b/src/BenchmarkDotNet/Attributes/Filters/OperatingSystemsArchitectureFilterAttribute.cs new file mode 100644 index 0000000000..5b7419a227 --- /dev/null +++ b/src/BenchmarkDotNet/Attributes/Filters/OperatingSystemsArchitectureFilterAttribute.cs @@ -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() { } + + /// if set to true, the architectures are enabled, if set to false, disabled + public OperatingSystemsArchitectureFilterAttribute(bool allowed, params Architecture[] architectures) + : base(new SimpleFilter(_ => + { + return allowed + ? architectures.Any(architecture => RuntimeInformation.OSArchitecture == architecture) + : architectures.All(architecture => RuntimeInformation.OSArchitecture != architecture); + })) + { + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Attributes/Filters/OperatingSystemsFilterAttribute.cs b/src/BenchmarkDotNet/Attributes/Filters/OperatingSystemsFilterAttribute.cs new file mode 100644 index 0000000000..203d25094e --- /dev/null +++ b/src/BenchmarkDotNet/Attributes/Filters/OperatingSystemsFilterAttribute.cs @@ -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, + /// + /// WebAssembly + /// + 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() { } + + /// if set to true, the OSes beloning to platforms are enabled, if set to false, disabled + public OperatingSystemsFilterAttribute(bool allowed, params OS[] platforms) + : base(new SimpleFilter(_ => + { + return allowed + ? platforms.Any(platform => RuntimeInformation.IsOSPlatform(Map(platform))) + : 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"); + } + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Attributes/Mutators/JobMutatorConfigBaseAttribute.cs b/src/BenchmarkDotNet/Attributes/Mutators/JobMutatorConfigBaseAttribute.cs index 518ae6e7b5..a4a4102f49 100644 --- a/src/BenchmarkDotNet/Attributes/Mutators/JobMutatorConfigBaseAttribute.cs +++ b/src/BenchmarkDotNet/Attributes/Mutators/JobMutatorConfigBaseAttribute.cs @@ -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 diff --git a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs index 6ee7467d81..fdca381fc8 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs @@ -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()).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(helperMethods, "GlobalSetup"); - var globalCleanupMethods = GetAttributedMethods(helperMethods, "GlobalCleanup"); - var iterationSetupMethods = GetAttributedMethods(helperMethods, "IterationSetup"); - var iterationCleanupMethods = GetAttributedMethods(helperMethods, "IterationCleanup"); - - var targetMethods = benchmarkMethods.Where(method => method.HasAttribute()).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(allPublicMethods, "GlobalSetup"); + var globalCleanupMethods = GetAttributedMethods(allPublicMethods, "GlobalCleanup"); + var iterationSetupMethods = GetAttributedMethods(allPublicMethods, "IterationSetup"); + var iterationCleanupMethods = GetAttributedMethods(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(); + 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(); - var assemblyAttributes = type.GetTypeInfo().Assembly.GetCustomAttributes().OfType(); - 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(); + var assemblyAttributes = type.Assembly.GetCustomAttributes().OfType(); + + 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(); + + 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); } @@ -251,7 +251,7 @@ private static string[] GetCategories(MethodInfo method) return attributes.SelectMany(attr => attr.Categories).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } - private static ImmutableArray GetFilteredBenchmarks(IList benchmarks, IList filters) + private static ImmutableArray GetFilteredBenchmarks(IEnumerable benchmarks, IEnumerable filters) => benchmarks.Where(benchmark => filters.All(filter => filter.Predicate(benchmark))).ToImmutableArray(); private static void AssertMethodHasCorrectSignature(string methodType, MethodInfo methodInfo) diff --git a/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs b/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs index 1a60178e9e..373d956b77 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs @@ -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(); diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs index f8f827462a..d7b2311a48 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs @@ -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) { diff --git a/tests/BenchmarkDotNet.Tests/Configs/ConfigPerMethodTests.cs b/tests/BenchmarkDotNet.Tests/Configs/ConfigPerMethodTests.cs new file mode 100644 index 0000000000..f11fbb7e42 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Configs/ConfigPerMethodTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Running; +using Xunit; + +namespace BenchmarkDotNet.Tests.Configs +{ + public class ConfigPerMethodTests + { + [Fact] + public void PerMethodConfigsAreRespected() + { + var never = BenchmarkConverter.TypeToBenchmarks(typeof(WithBenchmarkThatShouldNeverRun)); + + Assert.Empty(never.BenchmarksCases); + + var always = BenchmarkConverter.TypeToBenchmarks(typeof(WithBenchmarkThatShouldAlwaysRun)); + + Assert.NotEmpty(always.BenchmarksCases); + } + + public class ConditionalRun : FilterConfigBaseAttribute + { + public ConditionalRun(bool value) : base(new SimpleFilter(_ => value)) { } + } + + public class WithBenchmarkThatShouldNeverRun + { + [Benchmark] + [ConditionalRun(false)] + public void Method() { } + } + + public class WithBenchmarkThatShouldAlwaysRun + { + [Benchmark] + [ConditionalRun(true)] + public void Method() { } + } + + [Fact] + public void CanEnableOrDisableTheBenchmarkPerOperatingSystem() + { + var allowedForWindows = BenchmarkConverter.TypeToBenchmarks(typeof(WithBenchmarkAllowedForWindows)); + var notAllowedForWindows = BenchmarkConverter.TypeToBenchmarks(typeof(WithBenchmarkNotAllowedForWindows)); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.NotEmpty(allowedForWindows.BenchmarksCases); + Assert.Empty(notAllowedForWindows.BenchmarksCases); + } + else + { + Assert.Empty(allowedForWindows.BenchmarksCases); + Assert.NotEmpty(notAllowedForWindows.BenchmarksCases); + } + } + + public class WithBenchmarkAllowedForWindows + { + [Benchmark] + [OperatingSystemsFilter(allowed: true, OS.Windows)] + public void Method() { } + } + + public class WithBenchmarkNotAllowedForWindows + { + [Benchmark] + [OperatingSystemsFilter(allowed: false, OS.Windows)] + public void Method() { } + } + + [Fact] + public void CanEnableOrDisableTheBenchmarkPerOperatingSystemArchitecture() + { + var allowed = BenchmarkConverter.TypeToBenchmarks(typeof(WithBenchmarkAllowedForX64)); + var notallowed = BenchmarkConverter.TypeToBenchmarks(typeof(WithBenchmarkNotAllowedForX64)); + + if (RuntimeInformation.OSArchitecture == Architecture.X64) + { + Assert.NotEmpty(allowed.BenchmarksCases); + Assert.Empty(notallowed.BenchmarksCases); + } + else + { + Assert.Empty(allowed.BenchmarksCases); + Assert.NotEmpty(notallowed.BenchmarksCases); + } + } + + public class WithBenchmarkAllowedForX64 + { + [Benchmark] + [OperatingSystemsArchitectureFilter(allowed: true, Architecture.X64)] + public void Method() { } + } + + public class WithBenchmarkNotAllowedForX64 + { + [Benchmark] + [OperatingSystemsArchitectureFilter(allowed: false, Architecture.X64)] + public void Method() { } + } + + [Fact] + public void CanEnableOrDisableMemoryRandomizationPerMethod() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(WithMemoryRandomization)).BenchmarksCases; + + Assert.Equal(2, benchmarks.Length); + var disabled = benchmarks.Single(benchmark => benchmark.Descriptor.WorkloadMethod.Name == nameof(WithMemoryRandomization.DisabledByDefault)); + Assert.False(disabled.Job.Run.MemoryRandomization); + var enabled = benchmarks.Single(benchmark => benchmark.Descriptor.WorkloadMethod.Name == nameof(WithMemoryRandomization.EnabledWithAttributeOnMethod)); + Assert.True(enabled.Job.Run.MemoryRandomization); + } + + public class WithMemoryRandomization + { + [Benchmark] + public void DisabledByDefault() { } + + [Benchmark] + [MemoryRandomization(true)] + public void EnabledWithAttributeOnMethod() { } + } + } +} \ No newline at end of file