diff --git a/src/BenchmarkDotNet/Columns/BaselineAllocationRatioColumn.cs b/src/BenchmarkDotNet/Columns/BaselineAllocationRatioColumn.cs new file mode 100644 index 0000000000..78f89adc90 --- /dev/null +++ b/src/BenchmarkDotNet/Columns/BaselineAllocationRatioColumn.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Mathematics; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using JetBrains.Annotations; + +namespace BenchmarkDotNet.Columns +{ + public class BaselineAllocationRatioColumn : BaselineCustomColumn + { + public override string Id => nameof(BaselineAllocationRatioColumn); + public override string ColumnName => "Alloc Ratio"; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, Statistics baseline, IReadOnlyDictionary baselineMetrics, + Statistics current, IReadOnlyDictionary currentMetrics, bool isBaseline) + { + double? ratio = GetAllocationRatio(currentMetrics, baselineMetrics); + double? invertedRatio = GetAllocationRatio(baselineMetrics, currentMetrics); + + if (ratio == null) + return "NA"; + + var cultureInfo = summary.GetCultureInfo(); + var ratioStyle = summary?.Style?.RatioStyle ?? RatioStyle.Value; + + bool advancedPrecision = IsNonBaselinesPrecise(summary, baselineMetrics, benchmarkCase); + switch (ratioStyle) + { + case RatioStyle.Value: + return ratio.Value.ToString(advancedPrecision ? "N3" : "N2", cultureInfo); + case RatioStyle.Percentage: + return isBaseline + ? "" + : ratio.Value >= 1.0 + ? "+" + ((ratio.Value - 1.0) * 100).ToString(advancedPrecision ? "N1" : "N0", cultureInfo) + "%" + : "-" + ((1.0 - ratio.Value) * 100).ToString(advancedPrecision ? "N1" : "N0", cultureInfo) + "%"; + case RatioStyle.Trend: + return isBaseline + ? "" + : ratio.Value >= 1.0 + ? ratio.Value.ToString(advancedPrecision ? "N3" : "N2", cultureInfo) + "x more" + : invertedRatio == null + ? "NA" + : invertedRatio.Value.ToString(advancedPrecision ? "N3" : "N2", cultureInfo) + "x less"; + default: + throw new ArgumentOutOfRangeException(nameof(summary), ratioStyle, "RatioStyle is not supported"); + } + } + + private static bool IsNonBaselinesPrecise(Summary summary, IReadOnlyDictionary baselineMetric, BenchmarkCase benchmarkCase) + { + string logicalGroupKey = summary.GetLogicalGroupKey(benchmarkCase); + var nonBaselines = summary.GetNonBaselines(logicalGroupKey); + return nonBaselines.Any(c => GetAllocationRatio(summary[c].Metrics, baselineMetric) is > 0 and < 0.01); + } + + private static double? GetAllocationRatio( + [CanBeNull] IReadOnlyDictionary current, + [CanBeNull] IReadOnlyDictionary baseline) + { + double? currentBytes = GetAllocatedBytes(current); + double? baselineBytes = GetAllocatedBytes(baseline); + + if (currentBytes == null || baselineBytes == null) + return null; + + if (baselineBytes == 0) + return null; + + return currentBytes / baselineBytes; + } + + private static double? GetAllocatedBytes([CanBeNull] IReadOnlyDictionary metrics) + { + var metric = metrics?.Values.FirstOrDefault(m => m.Descriptor is AllocatedMemoryMetricDescriptor); + return metric?.Value; + } + + public override ColumnCategory Category => ColumnCategory.Metric; //it should be displayed after Allocated column + public override int PriorityInCategory => AllocatedMemoryMetricDescriptor.Instance.PriorityInCategory + 1; + public override bool IsNumeric => true; + public override UnitType UnitType => UnitType.Dimensionless; + public override string Legend => "Allocated memory ratio distribution ([Current]/[Baseline])"; + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs b/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs index f75b791c79..019af41918 100644 --- a/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs +++ b/src/BenchmarkDotNet/Columns/BaselineCustomColumn.cs @@ -34,7 +34,7 @@ public abstract string GetValue(Summary summary, BenchmarkCase benchmarkCase, St public bool IsAvailable(Summary summary) => summary.HasBaselines(); public bool AlwaysShow => true; - public ColumnCategory Category => ColumnCategory.Baseline; + public virtual ColumnCategory Category => ColumnCategory.Baseline; public abstract int PriorityInCategory { get; } public abstract bool IsNumeric { get; } public abstract UnitType UnitType { get; } diff --git a/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs b/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs index 8e61fe1f2c..b69264901b 100644 --- a/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs +++ b/src/BenchmarkDotNet/Columns/DefaultColumnProvider.cs @@ -63,6 +63,11 @@ public IEnumerable GetColumns(Summary summary) bool hide = stdDevColumnValues.All(value => value == "0.00" || value == "0.01"); if (!hide) yield return BaselineRatioColumn.RatioStdDev; + + if (HasMemoryDiagnoser(summary)) + { + yield return new BaselineAllocationRatioColumn(); + } } } @@ -70,6 +75,11 @@ private static bool NeedToShow(Summary summary, Func check) { return summary.Reports != null && summary.Reports.Any(r => r.ResultStatistics != null && check(r.ResultStatistics)); } + + private static bool HasMemoryDiagnoser(Summary summary) + { + return summary.BenchmarksCases.Any(c => c.Config.HasMemoryDiagnoser()); + } } private class ParamsColumnProvider : IColumnProvider diff --git a/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs b/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs new file mode 100644 index 0000000000..0ff2a4bc55 --- /dev/null +++ b/src/BenchmarkDotNet/Diagnosers/AllocatedMemoryMetricDescriptor.cs @@ -0,0 +1,20 @@ +using System; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; + +namespace BenchmarkDotNet.Diagnosers +{ + internal class AllocatedMemoryMetricDescriptor : IMetricDescriptor + { + internal static readonly IMetricDescriptor Instance = new AllocatedMemoryMetricDescriptor(); + + public string Id => "Allocated Memory"; + public string DisplayName => "Allocated"; + public string Legend => "Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)"; + public string NumberFormat => "0.##"; + public UnitType UnitType => UnitType.Size; + public string Unit => SizeUnit.B.Name; + public bool TheGreaterTheBetter => false; + public int PriorityInCategory => GC.MaxGeneration + 1; + } +} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs index f23ba27d4e..da1b0ec971 100644 --- a/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs @@ -45,20 +45,6 @@ public IEnumerable ProcessResults(DiagnoserResults diagnoserResults) yield return new Metric(AllocatedMemoryMetricDescriptor.Instance, diagnoserResults.GcStats.GetBytesAllocatedPerOperation(diagnoserResults.BenchmarkCase)); } - private class AllocatedMemoryMetricDescriptor : IMetricDescriptor - { - internal static readonly IMetricDescriptor Instance = new AllocatedMemoryMetricDescriptor(); - - public string Id => "Allocated Memory"; - public string DisplayName => "Allocated"; - public string Legend => "Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)"; - public string NumberFormat => "0.##"; - public UnitType UnitType => UnitType.Size; - public string Unit => SizeUnit.B.Name; - public bool TheGreaterTheBetter => false; - public int PriorityInCategory => GC.MaxGeneration + 1; - } - private class GarbageCollectionsMetricDescriptor : IMetricDescriptor { internal static readonly IMetricDescriptor Gen0 = new GarbageCollectionsMetricDescriptor(0);