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) {