From 00993c5848b63334e0bbcd1610aac0bd87260404 Mon Sep 17 00:00:00 2001 From: bugRanger Date: Mon, 22 Jun 2026 18:58:18 +0700 Subject: [PATCH 01/10] Add hardware counter info --- .../Diagnosers/HardwareCounterInfo.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs diff --git a/src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs b/src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs new file mode 100644 index 0000000000..85168c6cbb --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Diagnostics.Tracing.Session; + +namespace BenchmarkDotNet.Diagnosers; + +[SuppressMessage("ReSharper", "IdentifierTypo")] +public readonly struct HardwareCounterInfo(string fullName, string shortName) : IEquatable +{ + public readonly string FullName = fullName; + public readonly string ShortName = shortName; + public readonly bool TheGreaterTheBetter = false; + + public static HardwareCounterInfo From(HardwareCounter counter) + { + return new HardwareCounterInfo(counter.ToString(), counter.ToShortName()); + } + + public static HardwareCounterInfo Parse(string counterName) + { + // Old + if (Enum.TryParse(counterName, true, out HardwareCounter counter)) + { + return new HardwareCounterInfo(counter.ToString(), counter.ToShortName()); + } + + // New + var availableCpuCounters = TraceEventProfileSources.GetInfo(); + if (availableCpuCounters.TryGetValue(counterName, out var cpuCounter)) + { + return new HardwareCounterInfo(cpuCounter.Name, counterName); + } + + throw new ArgumentException($"Unknown hardware counter {counterName}"); + } + + public bool Equals(HardwareCounterInfo other) => FullName == other.FullName; + + public override bool Equals(object? obj) => obj is HardwareCounterInfo other && Equals(other); + + public override int GetHashCode() => FullName != null ? FullName.GetHashCode() : 0; + + public static bool operator ==(HardwareCounterInfo left, HardwareCounterInfo right) => left.Equals(right); + + public static bool operator !=(HardwareCounterInfo left, HardwareCounterInfo right) => !left.Equals(right); +} \ No newline at end of file From 072de3b8f4e4903984eeaa5c3409c92df3b27acc Mon Sep 17 00:00:00 2001 From: bugRanger Date: Mon, 22 Jun 2026 22:47:49 +0700 Subject: [PATCH 02/10] Add hardware counter provider --- .../IntroHardwareCountersCustom.cs | 79 +++++++++++++++++++ .../EtwProfiler.cs | 10 ++- .../HardwareCounters.cs | 56 ++++++------- .../Configs/ConfigExtensions.cs | 3 + src/BenchmarkDotNet/Configs/DebugConfig.cs | 1 + src/BenchmarkDotNet/Configs/DefaultConfig.cs | 2 + src/BenchmarkDotNet/Configs/IConfig.cs | 1 + .../Configs/ImmutableConfig.cs | 4 + .../Configs/ImmutableConfigBuilder.cs | 2 + src/BenchmarkDotNet/Configs/ManualConfig.cs | 8 ++ .../DefaultHardwareCounterProvider.cs | 32 ++++++++ .../Diagnosers/IHardwareCounterProvider.cs | 6 ++ 12 files changed, 176 insertions(+), 28 deletions(-) create mode 100644 samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs create mode 100644 src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProvider.cs create mode 100644 src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs diff --git a/samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs new file mode 100644 index 0000000000..4c09b09ae5 --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs @@ -0,0 +1,79 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Running; + +namespace BenchmarkDotNet.Samples +{ + [HardwareCounters(HardwareCounter.CacheMisses)] + public class IntroHardwareCountersCustom + { + public static void Run() => + _ = BenchmarkRunner.Run(DefaultConfig.Default + .AddHardwareCounterProvider(CustomHardwareCounterProvider.Instance)); + + private const int N = 32767; + private readonly int[] sorted, unsorted; + + public IntroHardwareCountersCustom() + { + 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); + } +} + +public class CustomHardwareCounterProvider : IHardwareCounterProvider +{ + public static readonly IHardwareCounterProvider Instance = new CustomHardwareCounterProvider(); + + public IEnumerable GetVariants(HardwareCounter hardwareCounter) + { + switch (hardwareCounter) + { + case HardwareCounter.CacheMisses: + yield return "cache-references"; + yield return "cache-misses"; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(hardwareCounter), hardwareCounter, null); + } + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index 7d641b2357..37eef3c1f6 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs @@ -92,7 +92,15 @@ private void Start(DiagnoserActionParameters parameters) { var counters = benchmarkToCounters[parameters.BenchmarkCase] = parameters.Config .GetHardwareCounters() - .Select(counter => HardwareCounters.FromCounter(counter, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval)) + .SelectMany(counter => + { + var counterVariants = parameters.Config + .GetHardwareCounterProviders() + .SelectMany(x => x.GetVariants(counter)); + + return HardwareCounters.FromCounter(counter, counterVariants, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval); + }) + .Distinct() .ToArray(); if (counters.Any()) // we need to enable the counters before starting the kernel session diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs index a58706917f..b31132c088 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs @@ -8,23 +8,7 @@ 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 Func> GetProfileSources { get; internal set; } = TraceEventProfileSources.GetInfo; public static IEnumerable Validate(ValidationParameters validationParameters, bool mandatory) { @@ -43,21 +27,33 @@ 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 = GetProfileSources(); foreach (var hardwareCounter in validationParameters.Config.GetHardwareCounters()) { - if (!EtwTranslations.TryGetValue(hardwareCounter, out string counterName)) + string[] counterVariants = validationParameters.Config + .GetHardwareCounterProviders() + .SelectMany(x => x.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,11 +65,17 @@ public static IEnumerable Validate(ValidationParameters validat } } - internal static PreciseMachineCounter FromCounter(HardwareCounter counter, Func intervalSelector) + internal static IEnumerable FromCounter(HardwareCounter counter, IEnumerable counterVariants, + 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)); + var profileSourceInfos = GetProfileSources(); + foreach (var counterVariant in counterVariants) + { + if (profileSourceInfos.TryGetValue(counterVariant, out var profileSource)) + { + yield return new PreciseMachineCounter(profileSource.ID, profileSource.Name, counter, intervalSelector(profileSource)); + } + } } internal static void Enable(IEnumerable counters) diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index 9856e727b9..74de50500e 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 AddHardwareCounterProvider(this IConfig config, params IHardwareCounterProvider[] newHardwareCounterProviders) + => config.With(c => c.AddHardwareCounterProvider(newHardwareCounterProviders)); + [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..82b1c36634 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -53,6 +53,7 @@ public abstract class DebugConfig : IConfig public IEnumerable GetLoggers() => [ConsoleLogger.Default]; public IEnumerable GetDiagnosers() => []; public IEnumerable GetAnalysers() => []; + public IEnumerable GetHardwareCounterProviders() => [DefaultHardwareCounterProvider.Instance]; public IEnumerable GetHardwareCounters() => []; public IEnumerable GetEventProcessors() => []; public IEnumerable GetFilters() => []; diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index cd9e1c2fe4..e65de2042f 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -125,6 +125,8 @@ public string ArtifactsPath public IEnumerable GetDiagnosers() => []; + public IEnumerable GetHardwareCounterProviders() => [DefaultHardwareCounterProvider.Instance]; + public IEnumerable GetHardwareCounters() => []; public IEnumerable GetFilters() => []; diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index b554db3f7c..8fba94f7a6 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -23,6 +23,7 @@ public interface IConfig IEnumerable GetAnalysers(); IEnumerable GetJobs(); IEnumerable GetValidators(); + IEnumerable GetHardwareCounterProviders(); IEnumerable GetHardwareCounters(); IEnumerable GetFilters(); IEnumerable GetLogicalGroupRules(); diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index 5d644e7a93..ff26334281 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -26,6 +26,7 @@ public sealed class ImmutableConfig : IConfig private readonly ImmutableHashSet analysers; private readonly ImmutableHashSet validators; private readonly ImmutableHashSet jobs; + private readonly ImmutableArray hardwareCounterProviders; private readonly ImmutableHashSet hardwareCounters; private readonly ImmutableHashSet filters; private readonly ImmutableArray rules; @@ -35,6 +36,7 @@ public sealed class ImmutableConfig : IConfig internal ImmutableConfig( ImmutableArray uniqueColumnProviders, ImmutableHashSet uniqueLoggers, + ImmutableArray uniqueHardwareCounterProviders, ImmutableHashSet uniqueHardwareCounters, ImmutableHashSet uniqueDiagnosers, ImmutableArray uniqueExporters, @@ -58,6 +60,7 @@ internal ImmutableConfig( { columnProviders = uniqueColumnProviders; loggers = uniqueLoggers; + hardwareCounterProviders = uniqueHardwareCounterProviders; hardwareCounters = uniqueHardwareCounters; diagnosers = uniqueDiagnosers; exporters = uniqueExporters; @@ -97,6 +100,7 @@ internal ImmutableConfig( public IEnumerable GetAnalysers() => analysers; public IEnumerable GetJobs() => jobs; public IEnumerable GetValidators() => validators; + public IEnumerable GetHardwareCounterProviders() => hardwareCounterProviders; public IEnumerable GetHardwareCounters() => hardwareCounters; public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => rules; diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index c78efd6f02..3f35408bc7 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -42,6 +42,7 @@ public static ImmutableConfig Create(IConfig source) var uniqueLoggers = source.GetLoggers().ToImmutableHashSet(); var configAnalyse = new List(); + var uniqueHardwareCounterProviders = source.GetHardwareCounterProviders().ToImmutableArray(); var uniqueHardwareCounters = source.GetHardwareCounters().Where(counter => counter != HardwareCounter.NotSet).ToImmutableHashSet(); var uniqueDiagnosers = GetDiagnosers(source.GetDiagnosers(), uniqueHardwareCounters); var uniqueExporters = GetExporters(source.GetExporters(), uniqueDiagnosers, configAnalyse); @@ -59,6 +60,7 @@ public static ImmutableConfig Create(IConfig source) return new ImmutableConfig( uniqueColumnProviders, uniqueLoggers, + uniqueHardwareCounterProviders, uniqueHardwareCounters, uniqueDiagnosers, uniqueExporters, diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 1bf4518d7e..0ca68e72b9 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -27,6 +27,7 @@ public class ManualConfig : IConfig private readonly List analysers = []; private readonly List validators = []; private readonly List jobs = []; + private readonly List hardwareCounterProviders = [DefaultHardwareCounterProvider.Instance]; private readonly HashSet hardwareCounters = []; private readonly List filters = []; private readonly List logicalGroupRules = []; @@ -40,6 +41,7 @@ public class ManualConfig : IConfig public IEnumerable GetAnalysers() => analysers; public IEnumerable GetValidators() => validators; public IEnumerable GetJobs() => jobs; + public IEnumerable GetHardwareCounterProviders() => hardwareCounterProviders; public IEnumerable GetHardwareCounters() => hardwareCounters; public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => logicalGroupRules; @@ -166,6 +168,12 @@ public ManualConfig AddHardwareCounters(params HardwareCounter[] newHardwareCoun return this; } + public ManualConfig AddHardwareCounterProvider(params IHardwareCounterProvider[] newHardwareCounterProviders) + { + hardwareCounterProviders.AddRangeDistinct(newHardwareCounterProviders); + return this; + } + public ManualConfig AddFilter(params IFilter[] newFilters) { filters.AddRangeDistinct(newFilters); diff --git a/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProvider.cs b/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProvider.cs new file mode 100644 index 0000000000..5bbca03b36 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProvider.cs @@ -0,0 +1,32 @@ +namespace BenchmarkDotNet.Diagnosers; + +public class DefaultHardwareCounterProvider : IHardwareCounterProvider +{ + 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 IHardwareCounterProvider Instance = new DefaultHardwareCounterProvider(); + + 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/IHardwareCounterProvider.cs b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs new file mode 100644 index 0000000000..0d1ed444e9 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs @@ -0,0 +1,6 @@ +namespace BenchmarkDotNet.Diagnosers; + +public interface IHardwareCounterProvider +{ + IEnumerable GetVariants(HardwareCounter hardwareCounter); +} \ No newline at end of file From 895468b795485eedfab6b9807b6d35dfb7c5781f Mon Sep 17 00:00:00 2001 From: bugRanger Date: Tue, 23 Jun 2026 10:00:18 +0700 Subject: [PATCH 03/10] Fix a error in counter matching --- .../EtwProfiler.cs | 5 +---- .../HardwareCounters.cs | 7 ++----- src/BenchmarkDotNet/Configs/ConfigExtensions.cs | 4 ++-- src/BenchmarkDotNet/Configs/DebugConfig.cs | 1 + src/BenchmarkDotNet/Configs/DefaultConfig.cs | 5 +++-- src/BenchmarkDotNet/Configs/IConfig.cs | 3 +-- src/BenchmarkDotNet/Configs/ImmutableConfig.cs | 7 +++---- .../Configs/ImmutableConfigBuilder.cs | 3 +-- src/BenchmarkDotNet/Configs/ManualConfig.cs | 15 +++++++-------- 9 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index 37eef3c1f6..5a89b52c49 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs @@ -94,10 +94,7 @@ private void Start(DiagnoserActionParameters parameters) .GetHardwareCounters() .SelectMany(counter => { - var counterVariants = parameters.Config - .GetHardwareCounterProviders() - .SelectMany(x => x.GetVariants(counter)); - + var counterVariants = parameters.Config.HardwareCounterProvider.GetVariants(counter); return HardwareCounters.FromCounter(counter, counterVariants, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval); }) .Distinct() diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs index b31132c088..89198d8f7a 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs @@ -8,7 +8,7 @@ namespace BenchmarkDotNet.Diagnostics.Windows { public static class HardwareCounters { - public static Func> GetProfileSources { get; internal set; } = TraceEventProfileSources.GetInfo; + public static Func> GetProfileSources { get; set; } = TraceEventProfileSources.GetInfo; public static IEnumerable Validate(ValidationParameters validationParameters, bool mandatory) { @@ -31,10 +31,7 @@ public static IEnumerable Validate(ValidationParameters validat foreach (var hardwareCounter in validationParameters.Config.GetHardwareCounters()) { - string[] counterVariants = validationParameters.Config - .GetHardwareCounterProviders() - .SelectMany(x => x.GetVariants(hardwareCounter)) - .ToArray(); + string[] counterVariants = validationParameters.Config.HardwareCounterProvider.GetVariants(hardwareCounter).ToArray(); if (counterVariants.Length == 0) { diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index 74de50500e..ddf166399d 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -35,8 +35,8 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig WithOrderer(this IConfig config, IOrderer orderer) => config.With(m => m.WithOrderer(orderer)); - [PublicAPI] public static ManualConfig AddHardwareCounterProvider(this IConfig config, params IHardwareCounterProvider[] newHardwareCounterProviders) - => config.With(c => c.AddHardwareCounterProvider(newHardwareCounterProviders)); + [PublicAPI] public static ManualConfig WithHardwareCounterProvider(this IConfig config, IHardwareCounterProvider newHardwareCounterProvider) + => config.With(c => c.HardwareCounterProvider = newHardwareCounterProvider); [PublicAPI] public static ManualConfig AddHardwareCounters(this IConfig config, params HardwareCounter[] counters) => config.With(c => c.AddHardwareCounters(counters)); diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 82b1c36634..4c4ca2faaa 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -61,6 +61,7 @@ public abstract class DebugConfig : IConfig public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; + public IHardwareCounterProvider HardwareCounterProvider => DefaultHardwareCounterProvider.Instance; 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 e65de2042f..5f81f6a34c 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 IHardwareCounterProvider HardwareCounterProvider => DefaultHardwareCounterProvider.Instance; + public ConfigUnionRule UnionRule => ConfigUnionRule.Union; public CultureInfo? CultureInfo => null; @@ -125,8 +128,6 @@ public string ArtifactsPath public IEnumerable GetDiagnosers() => []; - public IEnumerable GetHardwareCounterProviders() => [DefaultHardwareCounterProvider.Instance]; - public IEnumerable GetHardwareCounters() => []; public IEnumerable GetFilters() => []; diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index 8fba94f7a6..6aa3c1a6a7 100644 --- a/src/BenchmarkDotNet/Configs/IConfig.cs +++ b/src/BenchmarkDotNet/Configs/IConfig.cs @@ -23,7 +23,6 @@ public interface IConfig IEnumerable GetAnalysers(); IEnumerable GetJobs(); IEnumerable GetValidators(); - IEnumerable GetHardwareCounterProviders(); IEnumerable GetHardwareCounters(); IEnumerable GetFilters(); IEnumerable GetLogicalGroupRules(); @@ -32,7 +31,7 @@ public interface IConfig IOrderer? Orderer { get; } ICategoryDiscoverer? CategoryDiscoverer { get; } - + IHardwareCounterProvider HardwareCounterProvider { get; } SummaryStyle? SummaryStyle { get; } ConfigUnionRule UnionRule { get; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs index ff26334281..0055c2d1b6 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -26,7 +26,6 @@ public sealed class ImmutableConfig : IConfig private readonly ImmutableHashSet analysers; private readonly ImmutableHashSet validators; private readonly ImmutableHashSet jobs; - private readonly ImmutableArray hardwareCounterProviders; private readonly ImmutableHashSet hardwareCounters; private readonly ImmutableHashSet filters; private readonly ImmutableArray rules; @@ -36,7 +35,6 @@ public sealed class ImmutableConfig : IConfig internal ImmutableConfig( ImmutableArray uniqueColumnProviders, ImmutableHashSet uniqueLoggers, - ImmutableArray uniqueHardwareCounterProviders, ImmutableHashSet uniqueHardwareCounters, ImmutableHashSet uniqueDiagnosers, ImmutableArray uniqueExporters, @@ -52,6 +50,7 @@ internal ImmutableConfig( CultureInfo cultureInfo, IOrderer orderer, ICategoryDiscoverer categoryDiscoverer, + IHardwareCounterProvider hardwareCounterProvider, SummaryStyle summaryStyle, ConfigOptions options, TimeSpan buildTimeout, @@ -60,7 +59,6 @@ internal ImmutableConfig( { columnProviders = uniqueColumnProviders; loggers = uniqueLoggers; - hardwareCounterProviders = uniqueHardwareCounterProviders; hardwareCounters = uniqueHardwareCounters; diagnosers = uniqueDiagnosers; exporters = uniqueExporters; @@ -76,6 +74,7 @@ internal ImmutableConfig( CultureInfo = cultureInfo; Orderer = orderer; CategoryDiscoverer = categoryDiscoverer; + HardwareCounterProvider = hardwareCounterProvider; SummaryStyle = summaryStyle; Options = options; BuildTimeout = buildTimeout; @@ -89,6 +88,7 @@ internal ImmutableConfig( public ConfigOptions Options { get; } public IOrderer Orderer { get; } public ICategoryDiscoverer CategoryDiscoverer { get; } + public IHardwareCounterProvider HardwareCounterProvider { get; } public SummaryStyle SummaryStyle { get; } public TimeSpan BuildTimeout { get; } public WakeLockType WakeLock { get; } @@ -100,7 +100,6 @@ internal ImmutableConfig( public IEnumerable GetAnalysers() => analysers; public IEnumerable GetJobs() => jobs; public IEnumerable GetValidators() => validators; - public IEnumerable GetHardwareCounterProviders() => hardwareCounterProviders; public IEnumerable GetHardwareCounters() => hardwareCounters; public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => rules; diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index 3f35408bc7..9c32dd2868 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -42,7 +42,6 @@ public static ImmutableConfig Create(IConfig source) var uniqueLoggers = source.GetLoggers().ToImmutableHashSet(); var configAnalyse = new List(); - var uniqueHardwareCounterProviders = source.GetHardwareCounterProviders().ToImmutableArray(); var uniqueHardwareCounters = source.GetHardwareCounters().Where(counter => counter != HardwareCounter.NotSet).ToImmutableHashSet(); var uniqueDiagnosers = GetDiagnosers(source.GetDiagnosers(), uniqueHardwareCounters); var uniqueExporters = GetExporters(source.GetExporters(), uniqueDiagnosers, configAnalyse); @@ -60,7 +59,6 @@ public static ImmutableConfig Create(IConfig source) return new ImmutableConfig( uniqueColumnProviders, uniqueLoggers, - uniqueHardwareCounterProviders, uniqueHardwareCounters, uniqueDiagnosers, uniqueExporters, @@ -76,6 +74,7 @@ public static ImmutableConfig Create(IConfig source) source.CultureInfo ?? DefaultCultureInfo.Instance, source.Orderer ?? DefaultOrderer.Instance, source.CategoryDiscoverer ?? DefaultCategoryDiscoverer.Instance, + source.HardwareCounterProvider ?? DefaultHardwareCounterProvider.Instance, source.SummaryStyle ?? SummaryStyle.Default, source.Options, source.BuildTimeout, diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index 0ca68e72b9..ef31803658 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -27,7 +27,6 @@ public class ManualConfig : IConfig private readonly List analysers = []; private readonly List validators = []; private readonly List jobs = []; - private readonly List hardwareCounterProviders = [DefaultHardwareCounterProvider.Instance]; private readonly HashSet hardwareCounters = []; private readonly List filters = []; private readonly List logicalGroupRules = []; @@ -41,7 +40,6 @@ public class ManualConfig : IConfig public IEnumerable GetAnalysers() => analysers; public IEnumerable GetValidators() => validators; public IEnumerable GetJobs() => jobs; - public IEnumerable GetHardwareCounterProviders() => hardwareCounterProviders; public IEnumerable GetHardwareCounters() => hardwareCounters; public IEnumerable GetFilters() => filters; public IEnumerable GetLogicalGroupRules() => logicalGroupRules; @@ -54,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 IHardwareCounterProvider HardwareCounterProvider { get; set; } = DefaultHardwareCounterProvider.Instance; [PublicAPI] public SummaryStyle? SummaryStyle { get; set; } [PublicAPI] public TimeSpan BuildTimeout { get; set; } = DefaultConfig.Instance.BuildTimeout; [PublicAPI] public WakeLockType WakeLock { get; set; } = DefaultConfig.Instance.WakeLock; @@ -102,6 +101,12 @@ public ManualConfig WithCategoryDiscoverer(ICategoryDiscoverer categoryDiscovere return this; } + public ManualConfig WithHardwareCounterProvider(IHardwareCounterProvider hardwareCounterProvider) + { + HardwareCounterProvider = hardwareCounterProvider; + return this; + } + public ManualConfig WithBuildTimeout(TimeSpan buildTimeout) { BuildTimeout = buildTimeout; @@ -168,12 +173,6 @@ public ManualConfig AddHardwareCounters(params HardwareCounter[] newHardwareCoun return this; } - public ManualConfig AddHardwareCounterProvider(params IHardwareCounterProvider[] newHardwareCounterProviders) - { - hardwareCounterProviders.AddRangeDistinct(newHardwareCounterProviders); - return this; - } - public ManualConfig AddFilter(params IFilter[] newFilters) { filters.AddRangeDistinct(newFilters); From 9cb0f9eae8a0785ecb940261843e2ea3cfb1b94a Mon Sep 17 00:00:00 2001 From: bugRanger Date: Tue, 23 Jun 2026 18:12:09 +0700 Subject: [PATCH 04/10] Add hardware counter profile and provider --- .../IntroHardwareCountersCustom.cs | 79 ------------------- .../DefaultHardwareCounterProvider.cs | 18 +++++ .../EtwProfiler.cs | 16 ++-- .../HardwareCounters.cs | 27 +++---- .../IHardwareCounterProvider.cs | 11 +++ .../Configs/ConfigExtensions.cs | 4 +- src/BenchmarkDotNet/Configs/DebugConfig.cs | 3 +- src/BenchmarkDotNet/Configs/DefaultConfig.cs | 2 +- src/BenchmarkDotNet/Configs/IConfig.cs | 2 +- .../Configs/ImmutableConfig.cs | 6 +- .../Configs/ImmutableConfigBuilder.cs | 2 +- src/BenchmarkDotNet/Configs/ManualConfig.cs | 7 +- ...er.cs => DefaultHardwareCounterProfile.cs} | 4 +- .../Diagnosers/IHardwareCounterProfile.cs | 10 +++ .../Diagnosers/IHardwareCounterProvider.cs | 6 -- 15 files changed, 73 insertions(+), 124 deletions(-) delete mode 100644 samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs create mode 100644 src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs rename src/BenchmarkDotNet/Diagnosers/{DefaultHardwareCounterProvider.cs => DefaultHardwareCounterProfile.cs} (88%) create mode 100644 src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs delete mode 100644 src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs diff --git a/samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs deleted file mode 100644 index 4c09b09ae5..0000000000 --- a/samples/BenchmarkDotNet.Samples/IntroHardwareCountersCustom.cs +++ /dev/null @@ -1,79 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Running; - -namespace BenchmarkDotNet.Samples -{ - [HardwareCounters(HardwareCounter.CacheMisses)] - public class IntroHardwareCountersCustom - { - public static void Run() => - _ = BenchmarkRunner.Run(DefaultConfig.Default - .AddHardwareCounterProvider(CustomHardwareCounterProvider.Instance)); - - private const int N = 32767; - private readonly int[] sorted, unsorted; - - public IntroHardwareCountersCustom() - { - 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); - } -} - -public class CustomHardwareCounterProvider : IHardwareCounterProvider -{ - public static readonly IHardwareCounterProvider Instance = new CustomHardwareCounterProvider(); - - public IEnumerable GetVariants(HardwareCounter hardwareCounter) - { - switch (hardwareCounter) - { - case HardwareCounter.CacheMisses: - yield return "cache-references"; - yield return "cache-misses"; - break; - - default: - throw new ArgumentOutOfRangeException(nameof(hardwareCounter), hardwareCounter, null); - } - } -} \ 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..d2679c3d73 --- /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 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()); + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index 5a89b52c49..d88cacfb1e 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) @@ -92,16 +94,12 @@ private void Start(DiagnoserActionParameters parameters) { var counters = benchmarkToCounters[parameters.BenchmarkCase] = parameters.Config .GetHardwareCounters() - .SelectMany(counter => - { - var counterVariants = parameters.Config.HardwareCounterProvider.GetVariants(counter); - return HardwareCounters.FromCounter(counter, counterVariants, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval); - }) - .Distinct() + .SelectMany(counter => HardwareCounters.FromCounter(counter, parameters.Config.HardwareCounterProfile, HardwareCounterProvider, + 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 89198d8f7a..75d960ad15 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs @@ -8,9 +8,10 @@ namespace BenchmarkDotNet.Diagnostics.Windows { public static class HardwareCounters { - public static Func> GetProfileSources { get; set; } = TraceEventProfileSources.GetInfo; - - public static IEnumerable Validate(ValidationParameters validationParameters, bool mandatory) + public static IEnumerable Validate( + ValidationParameters validationParameters, + IHardwareCounterProvider provider, + bool mandatory) { if (!OsDetector.IsWindows()) { @@ -27,11 +28,11 @@ 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 = GetProfileSources(); + var availableCpuCounters = provider.GetAvailableCounters(); foreach (var hardwareCounter in validationParameters.Config.GetHardwareCounters()) { - string[] counterVariants = validationParameters.Config.HardwareCounterProvider.GetVariants(hardwareCounter).ToArray(); + string[] counterVariants = validationParameters.Config.HardwareCounterProfile.GetVariants(hardwareCounter).ToArray(); if (counterVariants.Length == 0) { @@ -62,11 +63,14 @@ public static IEnumerable Validate(ValidationParameters validat } } - internal static IEnumerable FromCounter(HardwareCounter counter, IEnumerable counterVariants, + public static IEnumerable FromCounter( + HardwareCounter counter, + IHardwareCounterProfile profile, + IHardwareCounterProvider provider, Func intervalSelector) { - var profileSourceInfos = GetProfileSources(); - foreach (var counterVariant in counterVariants) + var profileSourceInfos = provider.GetAvailableCounters(); + foreach (var counterVariant in profile.GetVariants(counter)) { if (profileSourceInfos.TryGetValue(counterVariant, out var profileSource)) { @@ -74,12 +78,5 @@ internal static IEnumerable FromCounter(HardwareCounter c } } } - - 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()); - } } } \ 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..281240e079 --- /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 counters); +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs index ddf166399d..28d1e98f92 100644 --- a/src/BenchmarkDotNet/Configs/ConfigExtensions.cs +++ b/src/BenchmarkDotNet/Configs/ConfigExtensions.cs @@ -35,8 +35,8 @@ public static class ConfigExtensions [PublicAPI] public static ManualConfig WithOrderer(this IConfig config, IOrderer orderer) => config.With(m => m.WithOrderer(orderer)); - [PublicAPI] public static ManualConfig WithHardwareCounterProvider(this IConfig config, IHardwareCounterProvider newHardwareCounterProvider) - => config.With(c => c.HardwareCounterProvider = newHardwareCounterProvider); + [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)); diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index 4c4ca2faaa..d78a57c169 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -53,7 +53,6 @@ public abstract class DebugConfig : IConfig public IEnumerable GetLoggers() => [ConsoleLogger.Default]; public IEnumerable GetDiagnosers() => []; public IEnumerable GetAnalysers() => []; - public IEnumerable GetHardwareCounterProviders() => [DefaultHardwareCounterProvider.Instance]; public IEnumerable GetHardwareCounters() => []; public IEnumerable GetEventProcessors() => []; public IEnumerable GetFilters() => []; @@ -61,7 +60,7 @@ public abstract class DebugConfig : IConfig public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; - public IHardwareCounterProvider HardwareCounterProvider => DefaultHardwareCounterProvider.Instance; + public IHardwareCounterProfile HardwareCounterProfile => DefaultHardwareCounterProfile.Instance; 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 5f81f6a34c..653d085d44 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -87,7 +87,7 @@ public IEnumerable GetValidators() public ICategoryDiscoverer? CategoryDiscoverer => null; - public IHardwareCounterProvider HardwareCounterProvider => DefaultHardwareCounterProvider.Instance; + public IHardwareCounterProfile HardwareCounterProfile => DefaultHardwareCounterProfile.Instance; public ConfigUnionRule UnionRule => ConfigUnionRule.Union; diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index 6aa3c1a6a7..d4aa57306d 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; } - IHardwareCounterProvider HardwareCounterProvider { 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 0055c2d1b6..2cbcf45ea3 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfig.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfig.cs @@ -50,7 +50,7 @@ internal ImmutableConfig( CultureInfo cultureInfo, IOrderer orderer, ICategoryDiscoverer categoryDiscoverer, - IHardwareCounterProvider hardwareCounterProvider, + IHardwareCounterProfile hardwareCounterProfile, SummaryStyle summaryStyle, ConfigOptions options, TimeSpan buildTimeout, @@ -74,7 +74,7 @@ internal ImmutableConfig( CultureInfo = cultureInfo; Orderer = orderer; CategoryDiscoverer = categoryDiscoverer; - HardwareCounterProvider = hardwareCounterProvider; + HardwareCounterProfile = hardwareCounterProfile; SummaryStyle = summaryStyle; Options = options; BuildTimeout = buildTimeout; @@ -88,7 +88,7 @@ internal ImmutableConfig( public ConfigOptions Options { get; } public IOrderer Orderer { get; } public ICategoryDiscoverer CategoryDiscoverer { get; } - public IHardwareCounterProvider HardwareCounterProvider { 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 9c32dd2868..5dac8d1e79 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -74,7 +74,7 @@ public static ImmutableConfig Create(IConfig source) source.CultureInfo ?? DefaultCultureInfo.Instance, source.Orderer ?? DefaultOrderer.Instance, source.CategoryDiscoverer ?? DefaultCategoryDiscoverer.Instance, - source.HardwareCounterProvider ?? DefaultHardwareCounterProvider.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 ef31803658..a5487cde02 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -52,7 +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 IHardwareCounterProvider HardwareCounterProvider { get; set; } = DefaultHardwareCounterProvider.Instance; + [PublicAPI] public IHardwareCounterProfile HardwareCounterProfile { get; set; } = DefaultHardwareCounterProfile.Instance; [PublicAPI] public SummaryStyle? SummaryStyle { get; set; } [PublicAPI] public TimeSpan BuildTimeout { get; set; } = DefaultConfig.Instance.BuildTimeout; [PublicAPI] public WakeLockType WakeLock { get; set; } = DefaultConfig.Instance.WakeLock; @@ -101,9 +101,9 @@ public ManualConfig WithCategoryDiscoverer(ICategoryDiscoverer categoryDiscovere return this; } - public ManualConfig WithHardwareCounterProvider(IHardwareCounterProvider hardwareCounterProvider) + public ManualConfig WithHardwareCounterProfile(IHardwareCounterProfile hardwareCounterProfile) { - HardwareCounterProvider = hardwareCounterProvider; + HardwareCounterProfile = hardwareCounterProfile; return this; } @@ -227,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/DefaultHardwareCounterProvider.cs b/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProfile.cs similarity index 88% rename from src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProvider.cs rename to src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProfile.cs index 5bbca03b36..24f857a0e2 100644 --- a/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProvider.cs +++ b/src/BenchmarkDotNet/Diagnosers/DefaultHardwareCounterProfile.cs @@ -1,6 +1,6 @@ namespace BenchmarkDotNet.Diagnosers; -public class DefaultHardwareCounterProvider : IHardwareCounterProvider +public sealed class DefaultHardwareCounterProfile : IHardwareCounterProfile { private static readonly Dictionary EtwTranslations = new () @@ -20,7 +20,7 @@ private static readonly Dictionary EtwTranslations { HardwareCounter.BranchMispredictsRetired, "BranchMispredictsRetired" } }; - public static readonly IHardwareCounterProvider Instance = new DefaultHardwareCounterProvider(); + public static readonly IHardwareCounterProfile Instance = new DefaultHardwareCounterProfile(); public IEnumerable GetVariants(HardwareCounter hardwareCounter) { diff --git a/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs new file mode 100644 index 0000000000..25c723a559 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs @@ -0,0 +1,10 @@ +namespace BenchmarkDotNet.Diagnosers; + +/// +/// Предоставляет доступ к настройке профиля счетчика, +/// т.к. счетчик может иметь различное именование в зависимости от модели CPU или может иметь различные версии с более детальной информацией. +/// +public interface IHardwareCounterProfile +{ + IEnumerable GetVariants(HardwareCounter hardwareCounter); +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs deleted file mode 100644 index 0d1ed444e9..0000000000 --- a/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProvider.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BenchmarkDotNet.Diagnosers; - -public interface IHardwareCounterProvider -{ - IEnumerable GetVariants(HardwareCounter hardwareCounter); -} \ No newline at end of file From 23576f8950fab2ab138dc57e141f0e1113b56f55 Mon Sep 17 00:00:00 2001 From: bugRanger Date: Wed, 24 Jun 2026 17:52:28 +0700 Subject: [PATCH 05/10] Fix merge config --- src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs | 3 ++- src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs | 3 +-- src/BenchmarkDotNet/Configs/DebugConfig.cs | 2 +- src/BenchmarkDotNet/Configs/DefaultConfig.cs | 2 +- src/BenchmarkDotNet/Configs/IConfig.cs | 2 +- src/BenchmarkDotNet/Configs/ManualConfig.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index d88cacfb1e..2b66c9873b 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs @@ -92,9 +92,10 @@ public void DisplayResults(ILogger logger) private void Start(DiagnoserActionParameters parameters) { + var profileSourceInfos = HardwareCounterProvider.GetAvailableCounters(); var counters = benchmarkToCounters[parameters.BenchmarkCase] = parameters.Config .GetHardwareCounters() - .SelectMany(counter => HardwareCounters.FromCounter(counter, parameters.Config.HardwareCounterProfile, HardwareCounterProvider, + .SelectMany(counter => HardwareCounters.FromCounter(counter, parameters.Config.HardwareCounterProfile, profileSourceInfos, config.IntervalSelectors.TryGetValue(counter, out var selector) ? selector : GetInterval)) .ToArray(); diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs index 75d960ad15..e9f685703d 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/HardwareCounters.cs @@ -66,10 +66,9 @@ public static IEnumerable Validate( public static IEnumerable FromCounter( HardwareCounter counter, IHardwareCounterProfile profile, - IHardwareCounterProvider provider, + IReadOnlyDictionary profileSourceInfos, Func intervalSelector) { - var profileSourceInfos = provider.GetAvailableCounters(); foreach (var counterVariant in profile.GetVariants(counter)) { if (profileSourceInfos.TryGetValue(counterVariant, out var profileSource)) diff --git a/src/BenchmarkDotNet/Configs/DebugConfig.cs b/src/BenchmarkDotNet/Configs/DebugConfig.cs index d78a57c169..57fa479bc3 100644 --- a/src/BenchmarkDotNet/Configs/DebugConfig.cs +++ b/src/BenchmarkDotNet/Configs/DebugConfig.cs @@ -60,7 +60,7 @@ public abstract class DebugConfig : IConfig public IOrderer Orderer => DefaultOrderer.Instance; public ICategoryDiscoverer? CategoryDiscoverer => DefaultCategoryDiscoverer.Instance; - public IHardwareCounterProfile HardwareCounterProfile => DefaultHardwareCounterProfile.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 653d085d44..ae92ea4fa5 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -87,7 +87,7 @@ public IEnumerable GetValidators() public ICategoryDiscoverer? CategoryDiscoverer => null; - public IHardwareCounterProfile HardwareCounterProfile => DefaultHardwareCounterProfile.Instance; + public IHardwareCounterProfile? HardwareCounterProfile => null; public ConfigUnionRule UnionRule => ConfigUnionRule.Union; diff --git a/src/BenchmarkDotNet/Configs/IConfig.cs b/src/BenchmarkDotNet/Configs/IConfig.cs index d4aa57306d..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; } + IHardwareCounterProfile? HardwareCounterProfile { get; } SummaryStyle? SummaryStyle { get; } ConfigUnionRule UnionRule { get; } diff --git a/src/BenchmarkDotNet/Configs/ManualConfig.cs b/src/BenchmarkDotNet/Configs/ManualConfig.cs index a5487cde02..5380c3ab68 100644 --- a/src/BenchmarkDotNet/Configs/ManualConfig.cs +++ b/src/BenchmarkDotNet/Configs/ManualConfig.cs @@ -52,7 +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; } = DefaultHardwareCounterProfile.Instance; + [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; From 6dc484c3293117061412ee878a3dcebe07535dcc Mon Sep 17 00:00:00 2001 From: bugRanger Date: Wed, 24 Jun 2026 17:53:31 +0700 Subject: [PATCH 06/10] Add sample with hardware counter profile --- .../IntroHardwareCounters.cs | 6 ++ .../IntroHardwareCountersWithProfile.cs | 78 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs 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..86c9183edf --- /dev/null +++ b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs @@ -0,0 +1,78 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; + +namespace BenchmarkDotNet.Samples +{ + public class HardwareCounterProfile : IHardwareCounterProfile + { + public IEnumerable GetVariants(HardwareCounter hardwareCounter) + { + if (hardwareCounter == HardwareCounter.CacheMisses) + { + yield return "IcacheMisses"; + yield return "DcacheMisses"; + } + else + { + yield return hardwareCounter.ToString(); + } + } + } + + [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchInstructions)] + public class IntroHardwareCountersWithProfile + { + 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 From b374e60bd2f03dd7b0ceafd37068c7bbdcaab634 Mon Sep 17 00:00:00 2001 From: bugRanger Date: Wed, 24 Jun 2026 19:15:17 +0700 Subject: [PATCH 07/10] Fix doc comment for profile --- src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs index 25c723a559..89a3462967 100644 --- a/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs +++ b/src/BenchmarkDotNet/Diagnosers/IHardwareCounterProfile.cs @@ -1,9 +1,11 @@ namespace BenchmarkDotNet.Diagnosers; /// -/// Предоставляет доступ к настройке профиля счетчика, -/// т.к. счетчик может иметь различное именование в зависимости от модели CPU или может иметь различные версии с более детальной информацией. +/// 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); From fdfc89f079dce7b5af6d5e7cccb1057afc2487d6 Mon Sep 17 00:00:00 2001 From: bugRanger Date: Wed, 24 Jun 2026 19:15:23 +0700 Subject: [PATCH 08/10] Del unused code --- .../Diagnosers/HardwareCounterInfo.cs | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs diff --git a/src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs b/src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs deleted file mode 100644 index 85168c6cbb..0000000000 --- a/src/BenchmarkDotNet/Diagnosers/HardwareCounterInfo.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Diagnostics.Tracing.Session; - -namespace BenchmarkDotNet.Diagnosers; - -[SuppressMessage("ReSharper", "IdentifierTypo")] -public readonly struct HardwareCounterInfo(string fullName, string shortName) : IEquatable -{ - public readonly string FullName = fullName; - public readonly string ShortName = shortName; - public readonly bool TheGreaterTheBetter = false; - - public static HardwareCounterInfo From(HardwareCounter counter) - { - return new HardwareCounterInfo(counter.ToString(), counter.ToShortName()); - } - - public static HardwareCounterInfo Parse(string counterName) - { - // Old - if (Enum.TryParse(counterName, true, out HardwareCounter counter)) - { - return new HardwareCounterInfo(counter.ToString(), counter.ToShortName()); - } - - // New - var availableCpuCounters = TraceEventProfileSources.GetInfo(); - if (availableCpuCounters.TryGetValue(counterName, out var cpuCounter)) - { - return new HardwareCounterInfo(cpuCounter.Name, counterName); - } - - throw new ArgumentException($"Unknown hardware counter {counterName}"); - } - - public bool Equals(HardwareCounterInfo other) => FullName == other.FullName; - - public override bool Equals(object? obj) => obj is HardwareCounterInfo other && Equals(other); - - public override int GetHashCode() => FullName != null ? FullName.GetHashCode() : 0; - - public static bool operator ==(HardwareCounterInfo left, HardwareCounterInfo right) => left.Equals(right); - - public static bool operator !=(HardwareCounterInfo left, HardwareCounterInfo right) => !left.Equals(right); -} \ No newline at end of file From 9f856b54689d948fbeeaeb879811fb424d57c6bc Mon Sep 17 00:00:00 2001 From: bugRanger Date: Thu, 25 Jun 2026 09:40:52 +0700 Subject: [PATCH 09/10] Add integration tests for ETW --- BenchmarkDotNet.slnx | 1 + .../DefaultHardwareCounterProvider.cs | 6 +- .../IHardwareCounterProvider.cs | 2 +- .../App.config | 7 + ...enchmarkDotNet.IntegrationTests.ETW.csproj | 50 +++++++ .../HardwareCounterTests.cs | 130 ++++++++++++++++++ .../xunit.runner.json | 8 ++ 7 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 tests/BenchmarkDotNet.IntegrationTests.ETW/App.config create mode 100644 tests/BenchmarkDotNet.IntegrationTests.ETW/BenchmarkDotNet.IntegrationTests.ETW.csproj create mode 100644 tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests.ETW/xunit.runner.json 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/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs b/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs index d2679c3d73..afdad105b2 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/DefaultHardwareCounterProvider.cs @@ -9,10 +9,10 @@ public sealed class DefaultHardwareCounterProvider : IHardwareCounterProvider public Dictionary GetAvailableCounters() => TraceEventProfileSources.GetInfo(); - public void Configure(IEnumerable counters) + public void Configure(IEnumerable machineCounters) { TraceEventProfileSources.Set( // it's a must have to get the events enabled!! - counters.Select(counter => counter.ProfileSourceId).ToArray(), - counters.Select(counter => counter.Interval).ToArray()); + 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/IHardwareCounterProvider.cs b/src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs index 281240e079..76e07fa88c 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/IHardwareCounterProvider.cs @@ -7,5 +7,5 @@ public interface IHardwareCounterProvider { Dictionary GetAvailableCounters(); - void Configure(IEnumerable counters); + void Configure(IEnumerable machineCounters); } \ 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..6036ba1575 --- /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; + } + + /// + /// Подменяет реальные счетчики на кастомные. + /// + 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 From d2d9e922901370a6cc689431927dd3e1f6ac8a9e Mon Sep 17 00:00:00 2001 From: bugRanger Date: Thu, 25 Jun 2026 14:52:01 +0700 Subject: [PATCH 10/10] Add comment for sample and tests --- .../IntroHardwareCountersWithProfile.cs | 37 ++++++++++++------- .../HardwareCounterTests.cs | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs index 86c9183edf..6a6e580051 100644 --- a/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs +++ b/samples/BenchmarkDotNet.Samples/IntroHardwareCountersWithProfile.cs @@ -6,25 +6,36 @@ namespace BenchmarkDotNet.Samples { - public class HardwareCounterProfile : IHardwareCounterProfile + /// + /// 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 { - public IEnumerable GetVariants(HardwareCounter hardwareCounter) + /// + /// The profile replaces the value from with the one expected by your configuration. + /// + class HardwareCounterProfile : IHardwareCounterProfile { - if (hardwareCounter == HardwareCounter.CacheMisses) - { - yield return "IcacheMisses"; - yield return "DcacheMisses"; - } - else + public IEnumerable GetVariants(HardwareCounter hardwareCounter) { - yield return hardwareCounter.ToString(); + if (hardwareCounter == HardwareCounter.CacheMisses) + { + // Example for `AMD Ryzen 7 5700G` + yield return "IcacheMisses"; + yield return "DcacheMisses"; + } + else + { + yield return hardwareCounter.ToString(); + } } } - } - [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchInstructions)] - public class IntroHardwareCountersWithProfile - { public static void Run() => BenchmarkRunner.Run(DefaultConfig.Instance .AddJob(Job.Dry) diff --git a/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs b/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs index 6036ba1575..28855e24ad 100644 --- a/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests.ETW/HardwareCounterTests.cs @@ -45,7 +45,7 @@ private class CustomHardwareCounterProfile(params string[] variants) : IHardware } /// - /// Подменяет реальные счетчики на кастомные. + /// Replaces real counters with custom ones. /// private class FakeHardwareCounterProvider : IHardwareCounterProvider {