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..9488d581cb
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/BenchmarkDotNet.Diagnostics.Energy.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+ net8.0;net462
+ enable
+ $(NoWarn);1591
+ 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..7f18394782
--- /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..097e628a61
--- /dev/null
+++ b/src/BenchmarkDotNet.Diagnostics.Energy/EnergyDiagnoserAttribute.cs
@@ -0,0 +1,16 @@
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+
+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);
+ }
+ }
+}