Skip to content

Issue #1024: Calculate baseline by the fastest benchmark #2171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/BenchmarkDotNet/Attributes/AutomaticBaselineAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using BenchmarkDotNet.Configs;
using System;

namespace BenchmarkDotNet.Attributes
{
[AttributeUsage(AttributeTargets.Class)]
public class AutomaticBaselineAttribute : Attribute, IConfigSource
{
public IConfig Config { get; }

public AutomaticBaselineAttribute(AutomaticBaselineMode mode) => Config = ManualConfig.CreateEmpty().WithAutomaticBaseline(mode);
}
}
8 changes: 8 additions & 0 deletions src/BenchmarkDotNet/Configs/AutomaticBaselineMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BenchmarkDotNet.Configs
{
public enum AutomaticBaselineMode
{
None,
Fastest
}
}
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/DebugConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,7 @@ public string ArtifactsPath
public ConfigOptions Options => ConfigOptions.KeepBenchmarkFiles | ConfigOptions.DisableOptimizationsValidator;

public IReadOnlyList<Conclusion> ConfigAnalysisConclusion => emptyConclusion;

public AutomaticBaselineMode AutomaticBaselineMode { get; } = AutomaticBaselineMode.None;
}
}
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/DefaultConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,7 @@ public string ArtifactsPath
public IEnumerable<IFilter> GetFilters() => Array.Empty<IFilter>();

public IEnumerable<IColumnHidingRule> GetColumnHidingRules() => Array.Empty<IColumnHidingRule>();

public AutomaticBaselineMode AutomaticBaselineMode { get; } = AutomaticBaselineMode.None;
}
}
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/IConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@ public interface IConfig
/// Collect any errors or warnings when composing the configuration
/// </summary>
IReadOnlyList<Conclusion> ConfigAnalysisConclusion { get; }

AutomaticBaselineMode AutomaticBaselineMode { get; }
}
}
5 changes: 4 additions & 1 deletion src/BenchmarkDotNet/Configs/ImmutableConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ internal ImmutableConfig(
SummaryStyle summaryStyle,
ConfigOptions options,
TimeSpan buildTimeout,
IReadOnlyList<Conclusion> configAnalysisConclusion)
IReadOnlyList<Conclusion> configAnalysisConclusion,
AutomaticBaselineMode automaticBaselineMode)
{
columnProviders = uniqueColumnProviders;
loggers = uniqueLoggers;
Expand All @@ -74,6 +75,7 @@ internal ImmutableConfig(
Options = options;
BuildTimeout = buildTimeout;
ConfigAnalysisConclusion = configAnalysisConclusion;
AutomaticBaselineMode = automaticBaselineMode;
}

public ConfigUnionRule UnionRule { get; }
Expand All @@ -83,6 +85,7 @@ internal ImmutableConfig(
[NotNull] public IOrderer Orderer { get; }
public SummaryStyle SummaryStyle { get; }
public TimeSpan BuildTimeout { get; }
public AutomaticBaselineMode AutomaticBaselineMode { get; }

public IEnumerable<IColumnProvider> GetColumnProviders() => columnProviders;
public IEnumerable<IExporter> GetExporters() => exporters;
Expand Down
3 changes: 2 additions & 1 deletion src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public static ImmutableConfig Create(IConfig source)
source.SummaryStyle ?? SummaryStyle.Default,
source.Options,
source.BuildTimeout,
configAnalyse.AsReadOnly()
configAnalyse.AsReadOnly(),
source.AutomaticBaselineMode
);
}

Expand Down
11 changes: 11 additions & 0 deletions src/BenchmarkDotNet/Configs/ManualConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class ManualConfig : IConfig
[PublicAPI] public IOrderer Orderer { get; set; }
[PublicAPI] public SummaryStyle SummaryStyle { get; set; }
[PublicAPI] public TimeSpan BuildTimeout { get; set; } = DefaultConfig.Instance.BuildTimeout;
[PublicAPI] public AutomaticBaselineMode AutomaticBaselineMode { get; private set; }

public IReadOnlyList<Conclusion> ConfigAnalysisConclusion => emptyConclusion;

Expand Down Expand Up @@ -98,6 +99,12 @@ public ManualConfig WithBuildTimeout(TimeSpan buildTimeout)
return this;
}

public ManualConfig WithAutomaticBaseline(AutomaticBaselineMode automaticBaselineMode)
{
AutomaticBaselineMode = automaticBaselineMode;
return this;
}

[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method will soon be removed, please start using .AddColumn() instead.")]
public void Add(params IColumn[] newColumns) => AddColumn(newColumns);
Expand Down Expand Up @@ -254,6 +261,10 @@ public void Add(IConfig config)
columnHidingRules.AddRange(config.GetColumnHidingRules());
Options |= config.Options;
BuildTimeout = GetBuildTimeout(BuildTimeout, config.BuildTimeout);
if (config.AutomaticBaselineMode != AutomaticBaselineMode.None)
{
AutomaticBaselineMode = config.AutomaticBaselineMode;
}
}

/// <summary>
Expand Down
29 changes: 28 additions & 1 deletion src/BenchmarkDotNet/Reports/Summary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class Summary
private ImmutableDictionary<BenchmarkCase, BenchmarkReport> ReportMap { get; }
private BaseliningStrategy BaseliningStrategy { get; }
private bool? isMultipleRuntimes;
private readonly BenchmarkCase inferredBaselineBenchmarkCase;

public Summary(
string title,
Expand All @@ -62,13 +63,35 @@ public Summary(
DisplayPrecisionManager = new DisplayPrecisionManager(this);
Orderer = GetConfiguredOrdererOrDefaultOne(reports.Select(report => report.BenchmarkCase.Config));
BenchmarksCases = Orderer.GetSummaryOrder(reports.Select(report => report.BenchmarkCase).ToImmutableArray(), this).ToImmutableArray(); // we sort it first
inferredBaselineBenchmarkCase = GetFastestBenchmarkCase(reports);
Reports = BenchmarksCases.Select(b => ReportMap[b]).ToImmutableArray(); // we use sorted collection to re-create reports list
BaseliningStrategy = BaseliningStrategy.Create(BenchmarksCases);
Style = GetConfiguredSummaryStyleOrDefaultOne(BenchmarksCases).WithCultureInfo(cultureInfo);
Table = GetTable(Style);
AllRuntimes = BuildAllRuntimes(HostEnvironmentInfo, Reports);
}

private static BenchmarkCase GetFastestBenchmarkCase(ImmutableArray<BenchmarkReport> reports)
{
if (reports.Any(r => r.BenchmarkCase.Config.AutomaticBaselineMode == AutomaticBaselineMode.Fastest))
{
var fastestReport = reports.First();
if (fastestReport.ResultStatistics != null)
{
foreach (var report in reports.Skip(1).Where(r => r.ResultStatistics != null))
{
if (report.ResultStatistics.Mean < fastestReport.ResultStatistics.Mean)
{
fastestReport = report;
}
}
}
return fastestReport.BenchmarkCase;
}

return null;
}

[PublicAPI] public bool HasReport(BenchmarkCase benchmarkCase) => ReportMap.ContainsKey(benchmarkCase);

/// <summary>
Expand Down Expand Up @@ -133,7 +156,11 @@ public string GetLogicalGroupKey(BenchmarkCase benchmarkCase)
=> Orderer.GetLogicalGroupKey(BenchmarksCases, benchmarkCase);

public bool IsBaseline(BenchmarkCase benchmarkCase)
=> BaseliningStrategy.IsBaseline(benchmarkCase);
{
return inferredBaselineBenchmarkCase != null
? inferredBaselineBenchmarkCase == benchmarkCase
: BaseliningStrategy.IsBaseline(benchmarkCase);
}

[CanBeNull]
public BenchmarkCase GetBaseline(string logicalGroupKey)
Expand Down
11 changes: 11 additions & 0 deletions src/BenchmarkDotNet/Validators/BaselineValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ private BaselineValidator() { }

public IEnumerable<ValidationError> Validate(ValidationParameters input)
{
if (input.Config.AutomaticBaselineMode != Configs.AutomaticBaselineMode.None)
{
foreach (var benchmark in input.Benchmarks)
{
if (benchmark.Descriptor.Baseline || benchmark.Job.Meta.Baseline)
{
yield return new ValidationError(TreatsWarningsAsErrors, "You cannot use both pre-configured and automatic baseline configuration", benchmark);
}
}
}

var allBenchmarks = input.Benchmarks.ToImmutableArray();
var orderProvider = input.Config.Orderer;

Expand Down
32 changes: 32 additions & 0 deletions tests/BenchmarkDotNet.IntegrationTests/AutomaticBaselineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Tests.Mocks;
using System;
using System.Linq;
using Xunit;

namespace BenchmarkDotNet.IntegrationTests
{
public class AutomaticBaselineTests
{
[Fact]
public void AutomaticBaselineSelectionIsCorrect()
{
var config = ManualConfig.CreateEmpty()
.AddColumnProvider(DefaultColumnProviders.Instance)
.WithAutomaticBaseline(AutomaticBaselineMode.Fastest);

var summary = MockFactory.CreateSummary(config, hugeSd: true, Array.Empty<Metric>());
var table = summary.GetTable(SummaryStyle.Default);
var method = table.Columns.Single(c => c.Header == "Method");
var ratio = table.Columns.Single(c => c.Header == "Ratio");

Assert.Equal(2, method.Content.Length);
Assert.Equal(nameof(MockFactory.MockBenchmarkClass.Foo), method.Content[0]);
Assert.Equal(1.0, double.Parse(ratio.Content[0])); // A faster one, see measurements in MockFactory.cs
Assert.Equal(nameof(MockFactory.MockBenchmarkClass.Bar), method.Content[1]);
Assert.Equal(1.5, double.Parse(ratio.Content[1]));
}
}
}