From 67749728ff357741768941ff637f58f114f6945d Mon Sep 17 00:00:00 2001
From: Azamat Suleimanov <>
Date: Fri, 27 Feb 2026 17:27:54 +0100
Subject: [PATCH 1/6] Linux RAPL counters #2284
---
BenchmarkDotNet.slnx | 1 +
.../BenchmarkDotNet.Diagnostics.Energy.csproj | 15 +++
.../EnergyCounter.cs | 23 ++++
.../EnergyCounterDiscovery.cs | 91 ++++++++++++++
.../EnergyCountersSetup.cs | 24 ++++
.../EnergyDiagnoser.cs | 113 ++++++++++++++++++
.../EnergyDiagnoserAttribute.cs | 17 +++
.../EnergyDiagnoserConfig.cs | 12 ++
.../LinuxEnergyCounter.cs | 68 +++++++++++
9 files changed, 364 insertions(+)
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounter.cs
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounterDiscovery.cs
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/EnergyCountersSetup.cs
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserConfig.cs
create mode 100644 src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs
diff --git a/BenchmarkDotNet.slnx b/BenchmarkDotNet.slnx
index 1d2b3755ad..015f7a59d7 100644
--- a/BenchmarkDotNet.slnx
+++ b/BenchmarkDotNet.slnx
@@ -13,6 +13,7 @@
+
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj b/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
new file mode 100644
index 0000000000..890e69a792
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ net8.0;net462
+ enable
+ BenchmarkDotNet.Diagnostics.Energy
+ BenchmarkDotNet.Diagnostics.Energy
+
+
+
+
+
+
+
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounter.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounter.cs
new file mode 100644
index 0000000000..78577520fe
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounter.cs
@@ -0,0 +1,23 @@
+namespace BenchmarkDotNet.Diagnosers
+{
+ internal abstract class EnergyCounter
+ {
+ public EnergyCounter(string name, string id)
+ {
+ Name = !string.IsNullOrEmpty(name) ? name : throw new ArgumentException(nameof(name));
+ Id = !string.IsNullOrEmpty(id) ? id : throw new ArgumentException(nameof(id));
+ }
+
+ public abstract (bool, string) TestRead();
+
+ public abstract void FixStart();
+
+ public abstract void FixFinish();
+
+ public abstract long GetValue();
+
+ public string Name { get; protected set; }
+
+ public string Id { get; protected set; }
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounterDiscovery.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounterDiscovery.cs
new file mode 100644
index 0000000000..708605d7b8
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCounterDiscovery.cs
@@ -0,0 +1,91 @@
+// #define FAKE_RAPL
+
+using System.Runtime.InteropServices;
+
+namespace BenchmarkDotNet.Diagnosers
+{
+ internal static class EnergyCounterDiscovery
+ {
+ public static IEnumerable Discover(EnergyCountersSetup setup)
+ {
+#if FAKE_RAPL
+ return Filter(GetFake(), setup);
+#else
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ return Filter(DiscoverLinux(), setup);
+ else
+ throw new NotImplementedException(string.Format("RAPL support for {0} is not implemented yet", RuntimeInformation.OSDescription));
+#endif
+ }
+
+ private static IEnumerable Filter(IEnumerable counters, EnergyCountersSetup setup)
+ {
+ switch (setup)
+ {
+ case EnergyCountersSetup.All:
+ return counters;
+
+ case EnergyCountersSetup.Default:
+ return counters.Where(c => c.Name == "core");
+
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ private static IEnumerable DiscoverLinux()
+ {
+ const int MAX_PACKAGES = int.MaxValue;
+ const int MAX_PACKAGE_UNITS = int.MaxValue;
+
+ for (int i = 0; i < MAX_PACKAGES; i++)
+ {
+ string path = $"/sys/class/powercap/intel-rapl/intel-rapl:{i}";
+ if (LinuxEnergyCounter.IsValid(path))
+ {
+ yield return LinuxEnergyCounter.FromPath(path);
+
+ for (int j = 0; j < MAX_PACKAGE_UNITS; j++)
+ {
+ path = $"/sys/class/powercap/intel-rapl/intel-rapl:{i}/intel-rapl:{i}:{j}";
+ if (LinuxEnergyCounter.IsValid(path))
+ yield return LinuxEnergyCounter.FromPath(path);
+ else
+ break;
+ }
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+#if FAKE_RAPL
+ private static IEnumerable GetFake()
+ {
+ yield return new FakeEnergyCounter("package-0", 17995193, "intel-rapl:0");
+ yield return new FakeEnergyCounter("core", 9955052, "intel-rapl:0/intel-rapl:0:0");
+ yield return new FakeEnergyCounter("dram", 1858455, "intel-rapl:0/intel-rapl:0:1");
+ yield return new FakeEnergyCounter("uncore", 773924, "intel-rapl:0/intel-rapl:0:2");
+ }
+
+ private class FakeEnergyCounter : EnergyCounter
+ {
+ private long _value;
+
+ public FakeEnergyCounter(string name, long value, string id) : base(name, id) {
+ _value = value;
+ }
+
+ public override (bool, string) TestRead() => (true, string.Empty);
+
+ public override void FixStart() {}
+
+ public override void FixFinish() {}
+
+ public override long GetValue() => _value;
+ }
+#endif
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCountersSetup.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCountersSetup.cs
new file mode 100644
index 0000000000..eb9e78d0c3
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyCountersSetup.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BenchmarkDotNet.Diagnosers
+{
+ ///
+ /// Energy counters setup
+ ///
+ public enum EnergyCountersSetup
+ {
+ ///
+ /// Default setup (core only)
+ ///
+ Default = 0,
+
+ ///
+ /// All discovered counters
+ ///
+ All = 1,
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
new file mode 100644
index 0000000000..24c1ee662b
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
@@ -0,0 +1,113 @@
+using System.Diagnostics;
+using BenchmarkDotNet.Analysers;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Engines;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Loggers;
+using BenchmarkDotNet.Reports;
+using BenchmarkDotNet.Running;
+using BenchmarkDotNet.Validators;
+
+namespace BenchmarkDotNet.Diagnosers
+{
+ public class EnergyDiagnoser : IDiagnoser
+ {
+ private EnergyCounter[] _counters;
+
+ private bool _validationFailed = false;
+
+ private const string DiagnoserId = nameof(EnergyDiagnoser);
+
+ public static readonly EnergyDiagnoser Default = new EnergyDiagnoser(new EnergyDiagnoserConfig());
+
+ public EnergyDiagnoser(EnergyDiagnoserConfig config) => Config = config;
+
+ public EnergyDiagnoserConfig Config { get; }
+
+ public RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.NoOverhead;
+
+ public IEnumerable Ids => new[] { DiagnoserId };
+
+ public IEnumerable Exporters => Array.Empty();
+
+ public IEnumerable Analysers => Array.Empty();
+
+ public void DisplayResults(ILogger logger) { }
+
+ public IEnumerable Validate(ValidationParameters validationParameters)
+ {
+ try
+ {
+ _counters = EnergyCounterDiscovery.Discover(Config.EnergyCountersSetup).ToArray();
+ if (_counters.Length == 0)
+ throw new Exception("No RAPL counters found (or not enough rights)");
+
+ }
+ catch (Exception e)
+ {
+ _validationFailed = true;
+ return [new ValidationError(false, e.Message)];
+ }
+
+ var errors = _counters.Select(ec => ec.TestRead()).Where(x => x.Item1 == false).Select(x => new ValidationError(false, x.Item2));
+ _validationFailed = errors.Any();
+ return errors;
+ }
+
+ public void Handle(HostSignal signal, DiagnoserActionParameters _)
+ {
+ if (_validationFailed)
+ return;
+
+ if (signal == HostSignal.BeforeActualRun)
+ {
+ for (int i = 0; i < _counters.Length; i++)
+ _counters[i].FixStart();
+ }
+ else if (signal == HostSignal.AfterActualRun)
+ {
+ for (int i = 0; i < _counters.Length; i++)
+ _counters[i].FixFinish();
+ }
+ }
+
+ public IEnumerable ProcessResults(DiagnoserResults diagnoserResults)
+ {
+ if (_validationFailed)
+ yield break;
+
+ long operations = diagnoserResults.Measurements.Where(m => m.IterationStage == IterationStage.Actual).Sum(m => m.Operations);
+ Debug.Assert(operations > 0);
+
+ int priority = 0;
+ foreach (var energyCounter in _counters.OrderBy(c => c.Id))
+ {
+ long uj = energyCounter.GetValue();
+ double avg_uj = operations > 0 && uj > 0 ? ((double)uj) / operations : 0.0;
+
+ yield return new Metric(new EnergyMetricDescriptor(priority++, energyCounter.Name, energyCounter.Id), avg_uj);
+ }
+ }
+
+ private class EnergyMetricDescriptor : IMetricDescriptor
+ {
+ public EnergyMetricDescriptor(int priority, string unitName, string id)
+ {
+ Id = id;
+ DisplayName = $"EC {unitName}";
+ Legend = $"Average energy consumption of unit {unitName}, uj/op";
+ PriorityInCategory = priority;
+ }
+
+ public string Id { get; }
+ public string DisplayName { get; }
+ public string Legend { get; }
+ public string NumberFormat => "#,000.000 uj";
+ public UnitType UnitType => UnitType.Dimensionless;
+ public string Unit => "uj";
+ public bool TheGreaterTheBetter => false;
+ public int PriorityInCategory { get; }
+ public bool GetIsAvailable(Metric metric) => metric.Value > 0;
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
new file mode 100644
index 0000000000..6e2e850d07
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
@@ -0,0 +1,17 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Diagnostics.Energy;
+
+namespace BenchmarkDotNet.Attributes
+{
+ [AttributeUsage(AttributeTargets.Class)]
+ public class EnergyDiagnoserAttribute : Attribute, IConfigSource
+ {
+ public IConfig Config { get; }
+
+ public EnergyDiagnoserAttribute(EnergyCountersSetup setup = EnergyCountersSetup.Default)
+ {
+ Config = ManualConfig.CreateEmpty().AddDiagnoser(new EnergyDiagnoser(new EnergyDiagnoserConfig(setup)));
+ }
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserConfig.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserConfig.cs
new file mode 100644
index 0000000000..4379f5f0ec
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserConfig.cs
@@ -0,0 +1,12 @@
+namespace BenchmarkDotNet.Diagnosers
+{
+ public class EnergyDiagnoserConfig
+ {
+ public EnergyDiagnoserConfig(EnergyCountersSetup setup = EnergyCountersSetup.Default)
+ {
+ EnergyCountersSetup = setup;
+ }
+
+ public EnergyCountersSetup EnergyCountersSetup { get; }
+ }
+}
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs b/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs
new file mode 100644
index 0000000000..4d06505223
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics;
+
+namespace BenchmarkDotNet.Diagnosers
+{
+ ///
+ /// Energy counter which reads from /sys/class/powercap/intel-rapl/**
+ ///
+ internal class LinuxEnergyCounter : EnergyCounter
+ {
+ private readonly string _energyPath;
+ private long _start;
+ private long _finish;
+
+ public LinuxEnergyCounter(string energyPath, string name, string id) : base(name, id)
+ {
+ _energyPath = !string.IsNullOrEmpty(energyPath) ? energyPath : throw new ArgumentException(nameof(energyPath));
+ }
+
+ public override (bool, string) TestRead()
+ {
+ try
+ {
+ string fileContents = File.ReadAllText(_energyPath);
+ bool good = long.Parse(fileContents) > 0;
+ return (true, string.Empty);
+ }
+ catch (Exception e)
+ {
+ return (false, e.Message);
+ }
+ }
+
+ public override void FixStart()
+ {
+ try
+ {
+ _start = long.Parse(File.ReadAllText(_energyPath));
+ }
+ catch { }
+ }
+
+ public override void FixFinish()
+ {
+ try
+ {
+ _finish = long.Parse(File.ReadAllText(_energyPath));
+ }
+ catch { }
+ }
+
+ public override long GetValue()
+ {
+ return _start > 0 && _finish > 0 ? (_finish - _start) : 0;
+ }
+
+ public static bool IsValid(string path)
+ {
+ return File.Exists(Path.Combine(path, "name")) && File.Exists(Path.Combine(path, "energy_uj"));
+ }
+
+ public static LinuxEnergyCounter FromPath(string path)
+ {
+ string name = File.ReadAllText(Path.Combine(path, "name")).Trim();
+ Debug.Assert(!string.IsNullOrEmpty(name));
+ return new LinuxEnergyCounter($"{path}/energy_uj", name, path);
+ }
+ }
+}
From 339aac9169fb9dac5cd71e8b329c3027c35efc01 Mon Sep 17 00:00:00 2001
From: Azamat Suleimanov <>
Date: Fri, 27 Feb 2026 18:07:53 +0100
Subject: [PATCH 2/6] ignore CS1591
---
.../BenchmarkDotNet.Diagnostics.Energy.csproj | 1 +
.../EnergyDiagnoserAttribute.cs | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj b/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
index 890e69a792..9488d581cb 100644
--- a/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
@@ -4,6 +4,7 @@
net8.0;net462
enable
+ $(NoWarn);1591
BenchmarkDotNet.Diagnostics.Energy
BenchmarkDotNet.Diagnostics.Energy
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
index 6e2e850d07..097e628a61 100644
--- a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
@@ -1,6 +1,5 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
-using BenchmarkDotNet.Diagnostics.Energy;
namespace BenchmarkDotNet.Attributes
{
From 089eb9c03234aa12ca0508b8dba47db0ba2a555c Mon Sep 17 00:00:00 2001
From: Azamat Suleimanov <>
Date: Fri, 27 Feb 2026 18:21:33 +0100
Subject: [PATCH 3/6] fix build
---
src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
index 24c1ee662b..7f18394782 100644
--- a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
@@ -12,7 +12,7 @@ namespace BenchmarkDotNet.Diagnosers
{
public class EnergyDiagnoser : IDiagnoser
{
- private EnergyCounter[] _counters;
+ private EnergyCounter[]? _counters;
private bool _validationFailed = false;
From ad90f82e74bbdb574725d99f1c09acea99da7b75 Mon Sep 17 00:00:00 2001
From: Azamat Suleimanov <>
Date: Mon, 2 Mar 2026 12:05:13 +0100
Subject: [PATCH 4/6] fix CS8602: Dereference of a possibly null reference
---
src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
index 7f18394782..26774bbf43 100644
--- a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
@@ -59,6 +59,10 @@ public void Handle(HostSignal signal, DiagnoserActionParameters _)
if (_validationFailed)
return;
+ Debug.Assert(_counters != null);
+ if (_counters == null)
+ throw new Exception("Unexpected: _counters == null");
+
if (signal == HostSignal.BeforeActualRun)
{
for (int i = 0; i < _counters.Length; i++)
From b6130479dcc880c9cc2601a471112b8403c831c4 Mon Sep 17 00:00:00 2001
From: Azamat Suleimanov <>
Date: Mon, 2 Mar 2026 12:19:45 +0100
Subject: [PATCH 5/6] fix error CS8604: Possible null reference argument
---
src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
index 26774bbf43..6a56dd8dd7 100644
--- a/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoser.cs
@@ -77,7 +77,7 @@ public void Handle(HostSignal signal, DiagnoserActionParameters _)
public IEnumerable ProcessResults(DiagnoserResults diagnoserResults)
{
- if (_validationFailed)
+ if (_validationFailed || _counters == null)
yield break;
long operations = diagnoserResults.Measurements.Where(m => m.IterationStage == IterationStage.Actual).Sum(m => m.Operations);
From bfb7b4ab08b2cac0cacb4e0b25034c69cacab63c Mon Sep 17 00:00:00 2001
From: Azamat Suleimanov <>
Date: Mon, 2 Mar 2026 12:22:27 +0100
Subject: [PATCH 6/6] use 'good'
---
src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs b/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs
index 4d06505223..e9bba4397c 100644
--- a/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/LinuxEnergyCounter.cs
@@ -22,7 +22,7 @@ public override (bool, string) TestRead()
{
string fileContents = File.ReadAllText(_energyPath);
bool good = long.Parse(fileContents) > 0;
- return (true, string.Empty);
+ return (good, string.Empty);
}
catch (Exception e)
{