Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" />
<PackageVersion Include="Codebelt.Bootstrapper.Console" Version="5.0.1" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.2" />
<PackageVersion Include="Codebelt.Bootstrapper.Console" Version="5.0.2" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.3" />
<PackageVersion Include="Cuemon.Core" Version="10.1.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MinVer" Version="6.0.0" />
Expand Down
116 changes: 105 additions & 11 deletions src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkProgram.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
using Codebelt.Bootstrapper.Console;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Filters;
using BenchmarkDotNet.Running;
using Codebelt.Bootstrapper.Console;
using Cuemon;
using Cuemon.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Codebelt.Extensions.BenchmarkDotNet.Console
{
/// <summary>
/// Entry point helper for hosting and running benchmarks using BenchmarkDotNet.
/// </summary>
/// <seealso cref="ConsoleProgram{TStartup}"/>
public class BenchmarkProgram : ConsoleProgram<BenchmarkWorker>
/// <seealso cref="MinimalConsoleProgram{BenchmarkProgram}"/>
public class BenchmarkProgram : MinimalConsoleProgram<BenchmarkProgram>
{
static BenchmarkProgram()
{
Expand Down Expand Up @@ -86,15 +94,101 @@ public static void Run<TWorkspace>(string[] args, Action<BenchmarkWorkspaceOptio
/// </remarks>
public static void Run<TWorkspace>(string[] args, Action<IServiceCollection> serviceConfigurator = null, Action<BenchmarkWorkspaceOptions> setup = null) where TWorkspace : class, IBenchmarkWorkspace
{
var hostBuilder = CreateHostBuilder(args);
hostBuilder.ConfigureServices(services =>
{
services.AddSingleton(new BenchmarkContext(args));
services.AddBenchmarkWorkspace<TWorkspace>(setup);
serviceConfigurator?.Invoke(services);
});
using var host = hostBuilder.Build();
var builder = CreateHostBuilder(args);

builder.Services.Configure<ConsoleLifetimeOptions>(o => o.SuppressStatusMessages = !IsDebugBuild);
builder.Services.AddSingleton(new BenchmarkContext(args));
builder.Services.AddBenchmarkWorkspace<TWorkspace>(setup);
serviceConfigurator?.Invoke(builder.Services);

using var host = builder.Build();
host.Run();
}

/// <summary>
/// Runs the actual benchmarks as envisioned by BenchmarkDotNet.
/// </summary>
/// <param name="serviceProvider">The service provider.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A completed task when benchmark execution has finished.</returns>
/// <remarks>
/// When arguments are provided, they are forwarded to <see cref="BenchmarkSwitcher"/> for selective execution.
/// After execution completes, artifact post-processing is performed.
/// </remarks>
public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var options = serviceProvider.GetRequiredService<BenchmarkWorkspaceOptions>();
var workspace = serviceProvider.GetRequiredService<IBenchmarkWorkspace>();
var assemblies = workspace.LoadBenchmarkAssemblies();
var context = serviceProvider.GetRequiredService<BenchmarkContext>();

if (options.SkipBenchmarksWithReports) { ConfigureBenchmarkDotNetFiltersForExistingReports(options, assemblies); }

try
{
ExecuteBenchmarks(assemblies, context, options);
}
finally
{
workspace.PostProcessArtifacts();
}

return Task.CompletedTask;
}

private static void ConfigureBenchmarkDotNetFiltersForExistingReports(BenchmarkWorkspaceOptions options, Assembly[] assemblies)
{
var benchmarkTypes = assemblies
.SelectMany(a => a.GetTypes().Where(t => t.Name.EndsWith("Benchmark", StringComparison.Ordinal)))
.ToList();

options.ConfigureBenchmarkDotNet(c => ApplyReportFilters(c, options, benchmarkTypes));
}

private static IConfig ApplyReportFilters(IConfig config, BenchmarkWorkspaceOptions options, List<Type> benchmarkTypes)
{
var tuningPath = BenchmarkWorkspace.GetReportsTuningPath(options);
if (!Directory.Exists(tuningPath)) { return config; }

foreach (var report in Directory.EnumerateFiles(tuningPath))
{
var matchingType = FindMatchingBenchmarkType(report, benchmarkTypes);
if (matchingType != null)
{
config = config.AddFilter(new SimpleFilter(bc => bc.Descriptor.Type != matchingType));
}
}

return config;
}

private static Type FindMatchingBenchmarkType(string reportPath, List<Type> benchmarkTypes)
{
var filename = Path.GetFileNameWithoutExtension(reportPath);
var potentialTypeFullName = filename.Split('-').FirstOrDefault();
if (string.IsNullOrWhiteSpace(potentialTypeFullName)) { return null; }

var potentialTypeName = potentialTypeFullName.Split('.').LastOrDefault();
if (string.IsNullOrWhiteSpace(potentialTypeName)) { return null; }

return benchmarkTypes.FirstOrDefault(t => t.Name.Equals(potentialTypeName, StringComparison.OrdinalIgnoreCase));
}

private static void ExecuteBenchmarks(Assembly[] assemblies, BenchmarkContext context, BenchmarkWorkspaceOptions options)
{
if (context.Args.Length == 0)
{
foreach (var assembly in assemblies)
{
BenchmarkRunner.Run(assembly, options.Configuration);
}
}
else
{
BenchmarkSwitcher
.FromAssemblies(assemblies)
.Run(context.Args, options.Configuration);
}
}
}
}
112 changes: 0 additions & 112 deletions src/Codebelt.Extensions.BenchmarkDotNet.Console/BenchmarkWorker.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@

var candidatePaths = Directory
.EnumerateFiles(tuningDir, $"*.{benchmarkProjectSuffix}.dll", SearchOption.AllDirectories)
.Where(path => path.IndexOf(buildSegment, StringComparison.OrdinalIgnoreCase) >= 0);
.Where(path => path.Contains(buildSegment, StringComparison.OrdinalIgnoreCase));

foreach (var path in candidatePaths)
{
Expand All @@ -142,7 +142,7 @@
continue;
}

assemblies.Add(Assembly.LoadFrom(path));

Check warning on line 145 in src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Replace this call to 'Assembly.LoadFrom' with 'Assembly.Load'. (https://rules.sonarsource.com/csharp/RSPEC-3885)

Check warning on line 145 in src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Replace this call to 'Assembly.LoadFrom' with 'Assembly.Load'. (https://rules.sonarsource.com/csharp/RSPEC-3885)

Check warning on line 145 in src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Replace this call to 'Assembly.LoadFrom' with 'Assembly.Load'. (https://rules.sonarsource.com/csharp/RSPEC-3885)

Check warning on line 145 in src/Codebelt.Extensions.BenchmarkDotNet/BenchmarkWorkspace.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Replace this call to 'Assembly.LoadFrom' with 'Assembly.Load'. (https://rules.sonarsource.com/csharp/RSPEC-3885)
}
catch
{
Expand Down
8 changes: 8 additions & 0 deletions src/Codebelt.Extensions.BenchmarkDotNet/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Using abstractions in public APIs promotes encapsulation, testability, and long-term maintainability, as prescribed by the .NET Framework Design Guidelines.", Scope = "member", Target = "~M:Codebelt.Extensions.BenchmarkDotNet.BenchmarkWorkspace.LoadAssemblies(System.String,System.String,System.String,System.String,System.Boolean)~System.Collections.Generic.IEnumerable{System.Reflection.Assembly}")]
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public void BenchmarkProgram_ShouldInheritFromConsoleProgram()
// Assert
Assert.NotNull(baseType);
Assert.True(baseType.IsGenericType);
Assert.Equal("ConsoleProgram`1", baseType.Name);
Assert.Equal("MinimalConsoleProgram`1", baseType.Name);

TestOutput.WriteLine($"BenchmarkProgram correctly inherits from: {baseType.FullName}");
}
Expand All @@ -263,7 +263,7 @@ public void BenchmarkProgram_ShouldUseCorrectGenericTypeParameter()
// Assert
Assert.NotNull(genericArguments);
Assert.Single(genericArguments);
Assert.Equal(typeof(BenchmarkWorker), genericArguments[0]);
Assert.Equal(typeof(BenchmarkProgram), genericArguments[0]);

TestOutput.WriteLine($"BenchmarkProgram uses correct generic type parameter: {genericArguments[0].Name}");
}
Expand Down
Loading
Loading