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