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); + } + } +}