diff --git a/BenchmarkDotNet.slnx b/BenchmarkDotNet.slnx index bc1d9c6fea..36e0388386 100644 --- a/BenchmarkDotNet.slnx +++ b/BenchmarkDotNet.slnx @@ -34,6 +34,7 @@ + diff --git a/samples/BenchmarkDotNet.Samples/IntroHardwareCounters.cs b/samples/BenchmarkDotNet.Samples/IntroHardwareCounters.cs index 76204479a0..70ba9b72d3 100644 --- a/samples/BenchmarkDotNet.Samples/IntroHardwareCounters.cs +++ b/samples/BenchmarkDotNet.Samples/IntroHardwareCounters.cs @@ -1,5 +1,8 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; namespace BenchmarkDotNet.Samples { @@ -8,6 +11,9 @@ namespace BenchmarkDotNet.Samples HardwareCounter.BranchInstructions)] public class IntroHardwareCounters { + public static void Run() => + BenchmarkRunner.Run(DefaultConfig.Instance.AddJob(Job.Dry)); + private const int N = 32767; private readonly int[] sorted, unsorted; diff --git a/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs new file mode 100644 index 0000000000..6a6e580051 --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs @@ -0,0 +1,89 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; + +namespace BenchmarkDotNet.Samples +{ + /// + /// Demonstration of benchmark setup using , which cannot be automatically matched to your configuration. + /// + /// + /// Before starting, make sure the values from are available on your configuration. + /// You can get a list of available counters using the command `tracelog.exe -profilesources Help`. + /// + [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchInstructions)] + public class IntroHardwareCountersWithProfile + { + /// + /// The profile replaces the value from with the one expected by your configuration. + /// + class HardwareCounterProfile : IHardwareCounterProfile + { + public IEnumerable GetVariants(HardwareCounter hardwareCounter) + { + if (hardwareCounter == HardwareCounter.CacheMisses) + { + // Example for `AMD Ryzen 7 5700G` + yield return "IcacheMisses"; + yield return "DcacheMisses"; + } + else + { + yield return hardwareCounter.ToString(); + } + } + } + + public static void Run() => + BenchmarkRunner.Run(DefaultConfig.Instance + .AddJob(Job.Dry) + .WithHardwareCounterProfile(new HardwareCounterProfile())); + + private const int N = 32767; + private readonly int[] sorted, unsorted; + + public IntroHardwareCountersWithProfile() + { + var random = new Random(0); + unsorted = new int[N]; + sorted = new int[N]; + for (int i = 0; i < N; i++) + sorted[i] = unsorted[i] = random.Next(256); + Array.Sort(sorted); + } + + private static int Branch(int[] data) + { + int sum = 0; + for (int i = 0; i < N; i++) + if (data[i] >= 128) + sum += data[i]; + return sum; + } + + private static int Branchless(int[] data) + { + int sum = 0; + for (int i = 0; i < N; i++) + { + int t = (data[i] - 128) >> 31; + sum += ~t & data[i]; + } + return sum; + } + + [Benchmark] + public int SortedBranch() => Branch(sorted); + + [Benchmark] + public int UnsortedBranch() => Branch(unsorted); + + [Benchmark] + public int SortedBranchless() => Branchless(sorted); + + [Benchmark] + public int UnsortedBranchless() => Branchless(unsorted); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs b/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs new file mode 100644 index 0000000000..afdad105b2 --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs @@ -0,0 +1,18 @@ +using BenchmarkDotNet.Diagnosers; +using Microsoft.Diagnostics.Tracing.Session; + +namespace BenchmarkDotNet.Diagnostics.Windows; + +public sealed class DefaultHardwareCounterProvider : IHardwareCounterProvider +{ + public static readonly DefaultHardwareCounterProvider Instance = new (); + + public Dictionary GetAvailableCounters() => TraceEventProfileSources.GetInfo(); + + public void Configure(IEnumerable machineCounters) + { + TraceEventProfileSources.Set( // it's a must have to get the events enabled!! + machineCounters.Select(counter => counter.ProfileSourceId).ToArray(), + machineCounters.Select(counter => counter.Interval).ToArray()); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index 7d641b2357..2b66c9873b 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs @@ -21,6 +21,8 @@ public class EtwProfiler : IDiagnoser, IHardwareCountersDiagnoser, IProfiler private readonly Dictionary benchmarkToEtlFile; private readonly Dictionary benchmarkToCounters; + public IHardwareCounterProvider HardwareCounterProvider { get; set; } = new DefaultHardwareCounterProvider(); + private Session kernelSession = default!; private Session userSession = default!; private Session heapSession = default!; @@ -55,7 +57,7 @@ public EtwProfiler(EtwProfilerConfig config) public RunMode GetRunMode(BenchmarkCase benchmarkCase) => runMode; public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) - => HardwareCounters.Validate(validationParameters, mandatory: false).ToAsyncEnumerable(); + => HardwareCounters.Validate(validationParameters, HardwareCounterProvider, mandatory: false).ToAsyncEnumerable(); public ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parameters, CancellationToken cancellationToken) { @@ -64,7 +66,7 @@ public ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parame Start(parameters); else if (signal == HostSignal.AfterProcessExit) Stop(parameters); - return new(); + return new (); } public IEnumerable ProcessResults(DiagnoserResults results) @@ -90,13 +92,15 @@ public void DisplayResults(ILogger logger) private void Start(DiagnoserActionParameters parameters) { + var profileSourceInfos = HardwareCounterProvider.GetAvailableCounters(); var counters = benchmarkToCounters[parameters.BenchmarkCase] = parameters.Config .GetHardwareCounters() - .Select(counter => HardwareCounters.FromCounter(counter, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval)) + .SelectMany(counter => HardwareCounters.FromCounter(counter, parameters.Config.HardwareCounterProfile, profileSourceInfos, + config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval)) .ToArray(); if (counters.Any()) // we need to enable the counters before starting the kernel session - HardwareCounters.Enable(counters); + HardwareCounterProvider.Configure(counters); try { diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs index a58706917f..e9f685703d 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs @@ -8,25 +8,10 @@ namespace BenchmarkDotNet.Diagnostics.Windows { public static class HardwareCounters { - private static readonly Dictionary EtwTranslations - = new Dictionary - { - { HardwareCounter.Timer, "Timer" }, - { HardwareCounter.TotalIssues, "TotalIssues" }, - { HardwareCounter.BranchInstructions, "BranchInstructions" }, - { HardwareCounter.CacheMisses, "CacheMisses" }, - { HardwareCounter.BranchMispredictions, "BranchMispredictions" }, - { HardwareCounter.TotalCycles, "TotalCycles" }, - { HardwareCounter.UnhaltedCoreCycles, "UnhaltedCoreCycles" }, - { HardwareCounter.InstructionRetired, "InstructionRetired" }, - { HardwareCounter.UnhaltedReferenceCycles, "UnhaltedReferenceCycles" }, - { HardwareCounter.LlcReference, "LLCReference" }, - { HardwareCounter.LlcMisses, "LLCMisses" }, - { HardwareCounter.BranchInstructionRetired, "BranchInstructionRetired" }, - { HardwareCounter.BranchMispredictsRetired, "BranchMispredictsRetired" } - }; - - public static IEnumerable Validate(ValidationParameters validationParameters, bool mandatory) + public static IEnumerable Validate( + ValidationParameters validationParameters, + IHardwareCounterProvider provider, + bool mandatory) { if (!OsDetector.IsWindows()) { @@ -43,21 +28,30 @@ public static IEnumerable Validate(ValidationParameters validat if (TraceEventSession.IsElevated() != true) yield return new ValidationError(true, "Must be elevated (Admin) to use ETW Kernel Session (required for Hardware Counters and EtwProfiler)."); - var availableCpuCounters = TraceEventProfileSources.GetInfo(); + var availableCpuCounters = provider.GetAvailableCounters(); foreach (var hardwareCounter in validationParameters.Config.GetHardwareCounters()) { - if (!EtwTranslations.TryGetValue(hardwareCounter, out string counterName)) + string[] counterVariants = validationParameters.Config.HardwareCounterProfile.GetVariants(hardwareCounter).ToArray(); + + if (counterVariants.Length == 0) { yield return new ValidationError(true, $"Counter {hardwareCounter} not recognized. " + - $"Please make sure that you are using counter available on your machine. " + - $"You can get the list of available counters by running `tracelog.exe -profilesources Help`"); + $"Please ensure that you are using a counter that is supported by your hardware counter provider. "); continue; } - if (!availableCpuCounters.ContainsKey(counterName)) - yield return new ValidationError(true, $"The counter {counterName} is not available. Please make sure you are Windows 8+ without Hyper-V"); + foreach (string counterVariant in counterVariants) + { + if (!availableCpuCounters.ContainsKey(counterVariant)) + { + yield return new ValidationError(true, + $"The counter {counterVariant} is not available. " + + $"Please make sure you are Windows 8+ without Hyper-V and that you are using counter available on your machine. " + + $"You can get the list of available counters by running `tracelog.exe -profilesources Help`"); + } + } } foreach (var benchmark in validationParameters.Benchmarks) @@ -69,18 +63,19 @@ public static IEnumerable Validate(ValidationParameters validat } } - internal static PreciseMachineCounter FromCounter(HardwareCounter counter, Func intervalSelector) + public static IEnumerable FromCounter( + HardwareCounter counter, + IHardwareCounterProfile profile, + IReadOnlyDictionary profileSourceInfos, + Func intervalSelector) { - var profileSource = TraceEventProfileSources.GetInfo()[EtwTranslations[counter]]; // it can't fail, diagnoser validates that first - - return new PreciseMachineCounter(profileSource.ID, profileSource.Name, counter, intervalSelector(profileSource)); - } - - internal static void Enable(IEnumerable counters) - { - TraceEventProfileSources.Set( // it's a must have to get the events enabled!! - counters.Select(counter => counter.ProfileSourceId).ToArray(), - counters.Select(counter => counter.Interval).ToArray()); + foreach (var counterVariant in profile.GetVariants(counter)) + { + if (profileSourceInfos.TryGetValue(counterVariant, out var profileSource)) + { + yield return new PreciseMachineCounter(profileSource.ID, profileSource.Name, counter, intervalSelector(profileSource)); + } + } } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs b/src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs new file mode 100644 index 0000000000..76e07fa88c --- /dev/null +++ b/src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs @@ -0,0 +1,11 @@ +using BenchmarkDotNet.Diagnosers; +using Microsoft.Diagnostics.Tracing.Session; + +namespace BenchmarkDotNet.Diagnostics.Windows; + +public interface IHardwareCounterProvider +{ + Dictionary GetAvailableCounters(); + + void Configure(IEnumerable machineCounters); +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index 9856e727b9..28d1e98f92 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -35,6 +35,9 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig WithOrderer(this IConfig config, IOrderer orderer) => config.With(m => m.WithOrderer(orderer)); + [PublicAPI] public static ManualConfig WithHardwareCounterProfile(this IConfig config, IHardwareCounterProfile newHardwareCounterProfile) + => config.With(c => c.WithHardwareCounterProfile(newHardwareCounterProfile)); + [PublicAPI] public static ManualConfig AddHardwareCounters(this IConfig config, params HardwareCounter[] counters) => config.With(c => c.AddHardwareCounters(counters)); [PublicAPI] public static ManualConfig AddFilter(this IConfig config, params IFilter[] filters) => config.With(c => c.AddFilter(filters)); diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 3f9b3cd2a7..57fa479bc3 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -60,6 +60,7 @@ public abstract class DebugConfig : IConfig public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; + public IHardwareCounterProfile? HardwareCounterProfile => null; public SummaryStyle SummaryStyle => SummaryStyle.Default; public ConfigUnionRule UnionRule => ConfigUnionRule.Union; public TimeSpan BuildTimeout => DefaultConfig.Instance.BuildTimeout; diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index cd9e1c2fe4..ae92ea4fa5 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -84,8 +84,11 @@ public IEnumerable GetValidators() } public IOrderer? Orderer => null; + public ICategoryDiscoverer? CategoryDiscoverer => null; + public IHardwareCounterProfile? HardwareCounterProfile => null; + public ConfigUnionRule UnionRule => ConfigUnionRule.Union; public CultureInfo? CultureInfo => null; diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index b554db3f7c..228a9ad59e 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -31,7 +31,7 @@ public interface IConfig IOrderer? Orderer { get; } ICategoryDiscoverer? CategoryDiscoverer { get; } - + IHardwareCounterProfile? HardwareCounterProfile { get; } SummaryStyle? SummaryStyle { get; } ConfigUnionRule UnionRule { get; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index 5d644e7a93..2cbcf45ea3 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -50,6 +50,7 @@ internal ImmutableConfig( CultureInfo cultureInfo, IOrderer orderer, ICategoryDiscoverer categoryDiscoverer, + IHardwareCounterProfile hardwareCounterProfile, SummaryStyle summaryStyle, ConfigOptions options, TimeSpan buildTimeout, @@ -73,6 +74,7 @@ internal ImmutableConfig( CultureInfo = cultureInfo; Orderer = orderer; CategoryDiscoverer = categoryDiscoverer; + HardwareCounterProfile = hardwareCounterProfile; SummaryStyle = summaryStyle; Options = options; BuildTimeout = buildTimeout; @@ -86,6 +88,7 @@ internal ImmutableConfig( public ConfigOptions Options { get; } public IOrderer Orderer { get; } public ICategoryDiscoverer CategoryDiscoverer { get; } + public IHardwareCounterProfile HardwareCounterProfile { get; } public SummaryStyle SummaryStyle { get; } public TimeSpan BuildTimeout { get; } public WakeLockType WakeLock { get; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index c78efd6f02..5dac8d1e79 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -74,6 +74,7 @@ public static ImmutableConfig Create(IConfig source) source.CultureInfo ?? DefaultCultureInfo.Instance, source.Orderer ?? DefaultOrderer.Instance, source.CategoryDiscoverer ?? DefaultCategoryDiscoverer.Instance, + source.HardwareCounterProfile ?? DefaultHardwareCounterProfile.Instance, source.SummaryStyle ?? SummaryStyle.Default, source.Options, source.BuildTimeout, diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 1bf4518d7e..5380c3ab68 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -52,6 +52,7 @@ public class ManualConfig : IConfig [PublicAPI] public CultureInfo? CultureInfo { get; set; } [PublicAPI] public IOrderer? Orderer { get; set; } [PublicAPI] public ICategoryDiscoverer? CategoryDiscoverer { get; set; } + [PublicAPI] public IHardwareCounterProfile? HardwareCounterProfile { get; set; } [PublicAPI] public SummaryStyle? SummaryStyle { get; set; } [PublicAPI] public TimeSpan BuildTimeout { get; set; } = DefaultConfig.Instance.BuildTimeout; [PublicAPI] public WakeLockType WakeLock { get; set; } = DefaultConfig.Instance.WakeLock; @@ -100,6 +101,12 @@ public ManualConfig WithCategoryDiscoverer(ICategoryDiscoverer categoryDiscovere return this; } + public ManualConfig WithHardwareCounterProfile(IHardwareCounterProfile hardwareCounterProfile) + { + HardwareCounterProfile = hardwareCounterProfile; + return this; + } + public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) { BuildTimeout = buildTimeout; @@ -220,6 +227,7 @@ public void Add(IConfig config) eventProcessors.AddRangeDistinct(config.GetEventProcessors()); Orderer = config.Orderer ?? Orderer; CategoryDiscoverer = config.CategoryDiscoverer ?? CategoryDiscoverer; + HardwareCounterProfile = config.HardwareCounterProfile ?? HardwareCounterProfile; ArtifactsPath = config.ArtifactsPath ?? ArtifactsPath; CultureInfo = config.CultureInfo ?? CultureInfo; SummaryStyle = config.SummaryStyle ?? SummaryStyle; diff --git a/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProfile.cs b/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProfile.cs new file mode 100644 index 0000000000..24f857a0e2 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProfile.cs @@ -0,0 +1,32 @@ +namespace BenchmarkDotNet.Diagnosers; + +public sealed class DefaultHardwareCounterProfile : IHardwareCounterProfile +{ + private static readonly Dictionary EtwTranslations + = new () + { + { HardwareCounter.Timer, "Timer" }, + { HardwareCounter.TotalIssues, "TotalIssues" }, + { HardwareCounter.BranchInstructions, "BranchInstructions" }, + { HardwareCounter.CacheMisses, "CacheMisses" }, + { HardwareCounter.BranchMispredictions, "BranchMispredictions" }, + { HardwareCounter.TotalCycles, "TotalCycles" }, + { HardwareCounter.UnhaltedCoreCycles, "UnhaltedCoreCycles" }, + { HardwareCounter.InstructionRetired, "InstructionRetired" }, + { HardwareCounter.UnhaltedReferenceCycles, "UnhaltedReferenceCycles" }, + { HardwareCounter.LlcReference, "LLCReference" }, + { HardwareCounter.LlcMisses, "LLCMisses" }, + { HardwareCounter.BranchInstructionRetired, "BranchInstructionRetired" }, + { HardwareCounter.BranchMispredictsRetired, "BranchMispredictsRetired" } + }; + + public static readonly IHardwareCounterProfile Instance = new DefaultHardwareCounterProfile(); + + public IEnumerable GetVariants(HardwareCounter hardwareCounter) + { + if (EtwTranslations.TryGetValue(hardwareCounter, out var translation)) + { + yield return translation; + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs new file mode 100644 index 0000000000..89a3462967 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs @@ -0,0 +1,12 @@ +namespace BenchmarkDotNet.Diagnosers; + +/// +/// Hardware counter profile. +/// +/// +/// Use a profile when the counter in the environment under test has a different name or can provide multiple counters with more detailed information. +/// +public interface IHardwareCounterProfile +{ + IEnumerable GetVariants(HardwareCounter hardwareCounter); +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests.ETW/App.config b/tests/BenchmarkDotNet.IntegrationTests.ETW/App.config new file mode 100644 index 0000000000..54a73e41cc --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests.ETW/App.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests.ETW/BenchmarkDotNet.IntegrationTests.ETW.csproj b/tests/BenchmarkDotNet.IntegrationTests.ETW/BenchmarkDotNet.IntegrationTests.ETW.csproj new file mode 100644 index 0000000000..f8da215038 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests.ETW/BenchmarkDotNet.IntegrationTests.ETW.csproj @@ -0,0 +1,50 @@ + + + + net8.0 + enable + enable + BenchmarkDotNet.IntegrationTests.ETW + true + BenchmarkDotNet.IntegrationTests.ETW + + + + + + + + Always + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + Always + + + + diff --git a/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs b/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs new file mode 100644 index 0000000000..28855e24ad --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs @@ -0,0 +1,130 @@ +using System.Collections.Immutable; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Diagnostics.Windows; +using BenchmarkDotNet.Jobs; +using Microsoft.Diagnostics.Tracing.Session; + +namespace BenchmarkDotNet.IntegrationTests.ETW; + +public class HardwareCounterTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +{ + [Fact] + public void CustomHardwareCounterProfileAreSupported() + { + // Arrange + string[] customCounterNames = + [ + "FakeCacheMisses1", + "FakeCacheMisses2", + "FakeCacheMisses3", + ]; + + var config = DefaultConfig.Instance + .AddJob(Job.Dry) + .WithOptions(ConfigOptions.DisableOptimizationsValidator) + .WithHardwareCounterProfile(new CustomHardwareCounterProfile(customCounterNames)) + .AddHardwareCounters(HardwareCounter.CacheMisses) + .AddDiagnoser(new EtwProfiler + { + HardwareCounterProvider = new FakeHardwareCounterProvider(customCounterNames) + }); + + // Act + var summary = CanExecute(config, fullValidation: false); + + // Assert + Assert.False(summary.HasCriticalValidationErrors, "The \"Summary\" should have NOT \"HasCriticalValidationErrors\""); + Assert.Empty(summary.ValidationErrors); + } + + private class CustomHardwareCounterProfile(params string[] variants) : IHardwareCounterProfile + { + public IEnumerable GetVariants(HardwareCounter hardwareCounter) => variants; + } + + /// + /// Replaces real counters with custom ones. + /// + private class FakeHardwareCounterProvider : IHardwareCounterProvider + { + private readonly Dictionary counters; + + public FakeHardwareCounterProvider(params string[] counterNames) + { + counters = TraceEventProfileSources.GetInfo(); + if (counters.Count == 0) + { + throw new Exception("No counters found"); + } + + var replaceCounters = counters.Values + .Where(c => c.Name.Contains("branch", StringComparison.CurrentCultureIgnoreCase)) + .ToDictionary(s => s.Name, s => s); + + if (replaceCounters.Count == 0) + { + throw new Exception("No counters found"); + } + + var appendCounters = new List(); + foreach (string counterName in counterNames.Where(counterName => !counters.ContainsKey(counterName))) + { + var profileSource = replaceCounters.Values.First(); + counters.Remove(profileSource.Name); + replaceCounters.Remove(profileSource.Name); + + appendCounters.Add(new ProfileSourceInfo + { + Name = counterName, + ID = profileSource.ID, + Interval = profileSource.Interval, + MinInterval = profileSource.MinInterval, + MaxInterval = profileSource.MaxInterval, + }); + } + + appendCounters.ForEach(counter => counters.Add(counter.Name, counter)); + } + + public Dictionary GetAvailableCounters() => counters; + + public void Configure(IEnumerable machineCounters) + { + foreach (var counter in machineCounters) + { + if (!counters.ContainsKey(counter.Name)) + { + throw new NotImplementedException("Not found counter: " + counter.Name); + } + } + + TraceEventProfileSources.Set( // it's a must have to get the events enabled!! + machineCounters.Select(counter => counter.ProfileSourceId).ToArray(), + machineCounters.Select(counter => counter.Interval).ToArray()); + } + } + + public class SimpleBenchmark + { + private const int N = 7; + private readonly int[] sorted = [0, 1, 2, 3, 4, 5, 6]; + private readonly int[] unsorted = [6, 5, 4, 3, 2, 1, 0]; + + private static int Branch(int[] data) + { + int sum = 0; + for (int i = 0; i < N; i++) + if (data[i] >= 128) + sum += data[i]; + return sum; + } + + [Benchmark] + public int SortedBranch() => Branch(sorted); + + [Benchmark] + public int UnsortedBranch() => Branch(unsorted); + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests.ETW/xunit.runner.json b/tests/BenchmarkDotNet.IntegrationTests.ETW/xunit.runner.json new file mode 100644 index 0000000000..9452165f14 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests.ETW/xunit.runner.json @@ -0,0 +1,8 @@ +{ + "shadowCopy": false, + "methodDisplay": "method", + "diagnosticMessages": true, + "longRunningTestSeconds": 60, + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} \ No newline at end of file