Skip to content
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
10 changes: 10 additions & 0 deletions docs/articles/configs/exporters.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ By default, files with results will be located in
`.\BenchmarkDotNet.Artifacts\results` directory, but this can be changed via the `ArtifactsPath` property in the `IConfig`.
Default exporters are: csv, html and markdown.

The exported files are named after the title of the summary they belong to, which by default is the full name of
the benchmarked type (or `BenchmarkRun-joined` when the results of many types are joined into a single summary).
You can override it via the `Title` property in the `IConfig` (`--title` on the command line); when the run reports
more than one summary, the name of the benchmarked type is appended to the title, so that the results of one type
don't overwrite the results of another:

```cs
ManualConfig.Create(DefaultConfig.Instance).WithTitle("MyBenchmarks"); // MyBenchmarks-report-github.md
```

---

[!include[IntroExport](../samples/IntroExport.md)]
Expand Down
1 change: 1 addition & 0 deletions docs/articles/guides/console-args.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ dotnet run -c Release -- --filter * --runtimes net6.0 net8.0 --statisticalTest 5
* `--anyCategories` Any Categories to run
* `--attribute` Run all methods with given attribute (applied to class or method)
* `--join` (Default: false) Prints single table with results for all benchmarks
* `--title` Custom title for the produced summaries and the base name of the result files exported for them
* `--keepFiles` (Default: false) Determines if all auto-generated files should be kept or removed after running the benchmarks.
* `--noOverwrite` (Default: false) Determines if the exported result files should not be overwritten (be default they are overwritten).
* `--counters` Hardware Counters
Expand Down
8 changes: 8 additions & 0 deletions docs/articles/samples/IntroJoin.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ If you are using `BenchmarkSwitcher` and want to run all the benchmarks with a c
--join --allCategories=IntroJoinA
```

By default the joined summary and its exported result files are named `BenchmarkRun-joined`.
You can override this with a custom title via the `--title` option (or `ManualConfig.WithTitle(...)` in code),
which is handy when you run many sets of joined benchmarks and want to tell the exports apart:

```
--join --allCategories=IntroJoinA --title=MyBenchmarks
```

### Output

```markdown
Expand Down
5 changes: 5 additions & 0 deletions src/BenchmarkDotNet/Configs/ConfigExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public static class ConfigExtensions
[PublicAPI] public static ManualConfig WithSummaryStyle(this IConfig config, SummaryStyle summaryStyle) => config.With(c => c.WithSummaryStyle(summaryStyle));

[PublicAPI] public static ManualConfig WithArtifactsPath(this IConfig config, string artifactsPath) => config.With(m => m.WithArtifactsPath(artifactsPath));

/// <summary>
/// sets the title of the produced summaries and the base name of the result files exported for them
/// </summary>
[PublicAPI] public static ManualConfig WithTitle(this IConfig config, string title) => config.With(m => m.WithTitle(title));
[PublicAPI] public static ManualConfig WithUnionRule(this IConfig config, ConfigUnionRule unionRule) => config.With(m => m.WithUnionRule(unionRule));
[PublicAPI] public static ManualConfig WithCultureInfo(this IConfig config, CultureInfo cultureInfo) => config.With(m => m.CultureInfo = cultureInfo);

Expand Down
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/DebugConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ public abstract class DebugConfig : IConfig

public string? ArtifactsPath => null; // DefaultConfig.ArtifactsPath will be used if the user does not specify it in explicit way

public string? Title => null; // the title derived from the benchmarked types will be used if the user does not specify it in explicit way

public CultureInfo? CultureInfo => null;
public IEnumerable<BenchmarkLogicalGroupRule> GetLogicalGroupRules() => [];

Expand Down
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet/Configs/DefaultConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public string ArtifactsPath
}
}

public string? Title => null;

public IReadOnlyList<Conclusion> ConfigAnalysisConclusion => emptyConclusion;

public IEnumerable<Job> GetJobs() => [];
Expand Down
8 changes: 8 additions & 0 deletions src/BenchmarkDotNet/Configs/IConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ public interface IConfig
/// </summary>
string? ArtifactsPath { get; }

/// <summary>
/// the title of the produced summaries and the base name of the result files exported for them;
/// when the run reports more than one summary, the name of the benchmarked type is appended to keep
/// their files apart. When not specified, the name of the benchmarked type is used
/// (or "BenchmarkRun-joined" when <see cref="ConfigOptions.JoinSummary"/> is set)
/// </summary>
string? Title { get; }

CultureInfo? CultureInfo { get; }

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/BenchmarkDotNet/Configs/ImmutableConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ internal ImmutableConfig(
ImmutableHashSet<EventProcessor> uniqueEventProcessors,
ConfigUnionRule unionRule,
string artifactsPath,
string? title,
CultureInfo cultureInfo,
IOrderer orderer,
ICategoryDiscoverer categoryDiscoverer,
Expand All @@ -70,6 +71,7 @@ internal ImmutableConfig(
eventProcessors = uniqueEventProcessors;
UnionRule = unionRule;
ArtifactsPath = artifactsPath;
Title = title;
CultureInfo = cultureInfo;
Orderer = orderer;
CategoryDiscoverer = categoryDiscoverer;
Expand All @@ -82,6 +84,7 @@ internal ImmutableConfig(

public ConfigUnionRule UnionRule { get; }
public string ArtifactsPath { get; }
public string? Title { get; }
public CultureInfo CultureInfo { get; }
public ConfigOptions Options { get; }
public IOrderer Orderer { get; }
Expand Down
1 change: 1 addition & 0 deletions src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static ImmutableConfig Create(IConfig source)
uniqueEventProcessors,
source.UnionRule,
source.ArtifactsPath ?? DefaultConfig.Instance.ArtifactsPath!,
source.Title,
source.CultureInfo ?? DefaultCultureInfo.Instance,
source.Orderer ?? DefaultOrderer.Instance,
source.CategoryDiscoverer ?? DefaultCategoryDiscoverer.Instance,
Expand Down
8 changes: 8 additions & 0 deletions src/BenchmarkDotNet/Configs/ManualConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class ManualConfig : IConfig
[PublicAPI] public ConfigOptions Options { get; set; }
[PublicAPI] public ConfigUnionRule UnionRule { get; set; } = ConfigUnionRule.Union;
[PublicAPI] public string? ArtifactsPath { get; set; }
[PublicAPI] public string? Title { get; set; }
[PublicAPI] public CultureInfo? CultureInfo { get; set; }
[PublicAPI] public IOrderer? Orderer { get; set; }
[PublicAPI] public ICategoryDiscoverer? CategoryDiscoverer { get; set; }
Expand Down Expand Up @@ -82,6 +83,12 @@ public ManualConfig WithArtifactsPath(string artifactsPath)
return this;
}

public ManualConfig WithTitle(string title)
{
Title = title;
return this;
}

public ManualConfig WithSummaryStyle(SummaryStyle summaryStyle)
{
SummaryStyle = summaryStyle;
Expand Down Expand Up @@ -221,6 +228,7 @@ public void Add(IConfig config)
Orderer = config.Orderer ?? Orderer;
CategoryDiscoverer = config.CategoryDiscoverer ?? CategoryDiscoverer;
ArtifactsPath = config.ArtifactsPath ?? ArtifactsPath;
Title = config.Title ?? Title;
CultureInfo = config.CultureInfo ?? CultureInfo;
SummaryStyle = config.SummaryStyle ?? SummaryStyle;
logicalGroupRules.AddRangeDistinct(config.GetLogicalGroupRules());
Expand Down
3 changes: 3 additions & 0 deletions src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public bool UseDisassemblyDiagnoser
[Option("join", Required = false, Default = false, HelpText = "Prints single table with results for all benchmarks")]
public bool Join { get; set; }

[Option("title", Required = false, HelpText = "Custom title for the produced summaries and the base name of the result files exported for them")]
public string? Title { get; set; }

[Option("keepFiles", Required = false, Default = false, HelpText = "Determines if all auto-generated files should be kept or removed after running the benchmarks.")]
public bool KeepBenchmarkFiles { get; set; }

Expand Down
3 changes: 3 additions & 0 deletions src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig? globalC
if (options.ArtifactsDirectory != null)
config.ArtifactsPath = options.ArtifactsDirectory.FullName;

if (options.Title.IsNotBlank())
config.Title = options.Title;

var filters = GetFilters(options).ToArray();
if (filters.Length > 1)
config.AddFilter(new UnionFilter(filters));
Expand Down
25 changes: 6 additions & 19 deletions src/BenchmarkDotNet/Exporters/ExporterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public abstract class ExporterBase : IExporter

public async ValueTask ExportAsync(Summary summary, ILogger logger, CancellationToken cancellationToken)
{
string fileName = GetFileName(summary);
string filePath = GetArtifactFullName(summary);
if (File.Exists(filePath))
{
Expand All @@ -28,7 +27,7 @@ public async ValueTask ExportAsync(Summary summary, ILogger logger, Cancellation
catch (IOException)
{
string uniqueString = DateTime.Now.ToString("yyyyMMdd-HHmmss");
string alternativeFilePath = $"{Path.Combine(summary.ResultsDirectoryPath, fileName)}-{FileCaption}{FileNameSuffix}-{uniqueString}.{FileExtension}";
string alternativeFilePath = $"{Path.Combine(summary.ResultsDirectoryPath, summary.Title)}-{FileCaption}{FileNameSuffix}-{uniqueString}.{FileExtension}";
logger.WriteLineError($"Could not overwrite file {filePath}. Exporting to {alternativeFilePath}");
filePath = alternativeFilePath;
}
Expand All @@ -41,23 +40,11 @@ public async ValueTask ExportAsync(Summary summary, ILogger logger, Cancellation
logger.WriteLineInfo($" {filePath.GetBaseName(Directory.GetCurrentDirectory())}");
}

/// <summary>
/// the exported files are named after the title of the summary, which is guaranteed to be unique
/// within a single run and valid for a path (see <see cref="Helpers.TitleHelper"/>)
/// </summary>
public string GetArtifactFullName(Summary summary)
{
string fileName = GetFileName(summary);
return $"{Path.Combine(summary.ResultsDirectoryPath, fileName)}-{FileCaption}{FileNameSuffix}.{FileExtension}";
}

private static string GetFileName(Summary summary)
{
// we can't use simple name here, because user might be running benchmarks for a library, which defines few types with the same name
// and reports the results per type, so every summary is going to contain just single benchmark
// and we can't tell here if there is a name conflict or not
var targets = summary.BenchmarksCases.Select(b => b.Descriptor.Type).Distinct().ToArray();

if (targets.Length == 1)
return FolderNameHelper.ToFolderName(targets.Single());

return summary.Title;
}
=> $"{Path.Combine(summary.ResultsDirectoryPath, summary.Title)}-{FileCaption}{FileNameSuffix}.{FileExtension}";
}
}
83 changes: 83 additions & 0 deletions src/BenchmarkDotNet/Helpers/TitleHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Running;

namespace BenchmarkDotNet.Helpers
{
/// <summary>
/// provides the titles that identify a run and every summary it produces
/// </summary>
/// <remarks>
/// the title of a summary is also the base name of every file exported for it, so the titles of the
/// summaries produced by a single run have to be unique, otherwise the results would overwrite each other #529
/// </remarks>
internal static class TitleHelper
{
internal const string JoinedSummaryTitle = "BenchmarkRun-joined";
internal const string MultipleTypesTitle = "BenchmarkRun";

private const int MinTruncatedLength = 3;

/// <summary>
/// the title of the entire run, used as the base name of its log file
/// </summary>
internal static string GetRunTitle(BenchmarkRunInfo[] benchmarkRunInfos, int desiredMaxLength = int.MaxValue)
{
string? customTitle = benchmarkRunInfos
.Select(benchmark => benchmark.Config.Title)
.FirstOrDefault(title => title.IsNotBlank());

if (customTitle.IsNotBlank())
return Truncate(Escape(customTitle), desiredMaxLength);

var uniqueTargetTypes = benchmarkRunInfos
.SelectMany(info => info.BenchmarksCases.Select(benchmark => benchmark.Descriptor.Type))
.Distinct()
.ToArray();

return Truncate(
uniqueTargetTypes.Length == 1 ? FolderNameHelper.ToFolderName(uniqueTargetTypes[0]) : MultipleTypesTitle,
desiredMaxLength);
}

/// <summary>
/// the title of a summary that reports the benchmarks of a single type,
/// where <paramref name="isTheOnlySummary"/> tells whether the run produces just this one
/// </summary>
internal static string GetSummaryTitle(BenchmarkRunInfo benchmarkRunInfo, bool isTheOnlySummary, int desiredMaxLength = int.MaxValue)
{
// few types might have the same name: A.Name and B.Name would both report "Name",
// so we use the namespace-qualified name to tell their results apart #529
string typeName = FolderNameHelper.ToFolderName(benchmarkRunInfo.Type);
string? customTitle = benchmarkRunInfo.Config.Title;

if (customTitle.IsBlank())
return Truncate(typeName, desiredMaxLength);

// the title is defined by the config, so every summary of the run would be given the same one
return Truncate(isTheOnlySummary ? Escape(customTitle) : $"{Escape(customTitle)}-{typeName}", desiredMaxLength);
}

/// <summary>
/// the title of the single summary that joins the results of all the types
/// </summary>
internal static string GetJoinedSummaryTitle(string? customTitle, int desiredMaxLength = int.MaxValue)
=> Truncate(customTitle.IsBlank() ? JoinedSummaryTitle : Escape(customTitle), desiredMaxLength);

// the title becomes a file name, so it can not contain characters that are invalid for a path
private static string Escape(string title) => FolderNameHelper.ToFolderName((object)title);

/// <summary>
/// shortens the title so that the paths of the artifacts named after it don't exceed the limits of the OS
/// </summary>
private static string Truncate(string title, int desiredMaxLength)
{
if (title.Length <= desiredMaxLength || desiredMaxLength < MinTruncatedLength)
return title;

int prefixLength = desiredMaxLength / 2;
int suffixLength = desiredMaxLength - prefixLength - 1;

return title.Substring(0, prefixLength) + "-" + title.Substring(title.Length - suffixLength, suffixLength);
}
}
}
4 changes: 2 additions & 2 deletions src/BenchmarkDotNet/Reports/Summary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ internal static Summary ValidationFailed(string title, string resultsDirectoryPa
=> new Summary(title, [], HostEnvironmentInfo.GetCurrent(), resultsDirectoryPath, logFilePath, TimeSpan.Zero,
DefaultCultureInfo.Instance, validationErrors ?? [], []);

internal static Summary Join(List<Summary> summaries, ClockSpan clockSpan)
internal static Summary Join(List<Summary> summaries, ClockSpan clockSpan, string? title = null)
=> new Summary(
$"BenchmarkRun-joined-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}",
title ?? TitleHelper.GetJoinedSummaryTitle(customTitle: null),
summaries.SelectMany(summary => summary.Reports).ToImmutableArray(),
HostEnvironmentInfo.GetCurrent(),
summaries.First().ResultsDirectoryPath,
Expand Down
Loading