Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace PatternKit.Benchmarks.Coverage;

internal static class BenchmarkRepository
{
public static string FindRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "PatternKit.slnx")))
return directory.FullName;

directory = directory.Parent;
}

throw new DirectoryNotFoundException("Could not find PatternKit repository root.");
}
}
37 changes: 37 additions & 0 deletions benchmarks/PatternKit.Benchmarks/Coverage/BenchmarkRoute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using PatternKit.Examples.ProductionReadiness;

namespace PatternKit.Benchmarks.Coverage;

public enum BenchmarkRoute
{
Fluent,
SourceGenerated
}

public enum BenchmarkPhase
{
Construction,
Execution
}

public sealed record PatternBenchmarkRoute(
string PatternName,
PatternFamily Family,
BenchmarkRoute Route,
BenchmarkPhase Phase,
string SourcePath,
string TestPath,
string DocumentationPath)
{
public override string ToString()
=> $"{Family}/{PatternName}/{Route}/{Phase}";
}

public sealed record GeneratorBenchmarkRoute(
string GeneratorName,
string SourcePath,
string TestPath,
string DocumentationPath)
{
public override string ToString() => GeneratorName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using PatternKit.Examples.ProductionReadiness;

namespace PatternKit.Benchmarks.Coverage;

public static class GeneratorBenchmarkCoverage
{
private static readonly Lazy<IReadOnlyList<GeneratorBenchmarkRoute>> LazyRoutes = new(CreateRoutes);

public static IReadOnlyList<GeneratorBenchmarkRoute> Routes => LazyRoutes.Value;

private static IReadOnlyList<GeneratorBenchmarkRoute> CreateRoutes()
{
var repositoryRoot = BenchmarkRepository.FindRoot();
var generatorRoot = Path.Combine(repositoryRoot, "src", "PatternKit.Generators");
var catalog = new PatternKitPatternCatalog();
var catalogBySource = catalog.Patterns
.Where(static pattern => pattern.Implementation.HasSourceGeneratedPath)
.GroupBy(static pattern => pattern.Implementation.GeneratorSourcePath!, StringComparer.OrdinalIgnoreCase)
.ToDictionary(static group => group.Key, static group => group.First(), StringComparer.OrdinalIgnoreCase);

return Directory
.EnumerateFiles(generatorRoot, "*Generator.cs", SearchOption.AllDirectories)
.Where(static path => !Path.GetFileName(path).StartsWith("Generate", StringComparison.Ordinal))
.OrderBy(static path => path, StringComparer.OrdinalIgnoreCase)
.Select(path =>
{
var relativePath = Path.GetRelativePath(repositoryRoot, path).Replace('\\', '/');
catalogBySource.TryGetValue(relativePath, out var pattern);
return new GeneratorBenchmarkRoute(
Path.GetFileNameWithoutExtension(path),
relativePath,
pattern?.Implementation.GeneratorTestPath ?? string.Empty,
pattern?.Implementation.GeneratorDocumentationPath ?? string.Empty);
})
.ToArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using BenchmarkDotNet.Attributes;

namespace PatternKit.Benchmarks.Coverage;

[BenchmarkCategory("Coverage", "GeneratorMatrix")]
public class GeneratorMatrixBenchmarks
{
private static readonly string RepositoryRoot = BenchmarkRepository.FindRoot();

[ParamsSource(nameof(GeneratorRoutes))]
public GeneratorBenchmarkRoute GeneratorRoute { get; set; } = default!;

public static IEnumerable<GeneratorBenchmarkRoute> GeneratorRoutes => GeneratorBenchmarkCoverage.Routes;

[Benchmark(Description = "Generator: source coverage route")]
[BenchmarkCategory("Generated", "Generator")]
public int Generator_Source_Route()
{
var sourcePath = Path.Combine(RepositoryRoot, GeneratorRoute.SourcePath.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(sourcePath))
throw new FileNotFoundException($"Generator source is missing: {GeneratorRoute.SourcePath}", sourcePath);

return GeneratorRoute.GeneratorName.Length + GeneratorRoute.SourcePath.Length;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using PatternKit.Examples.ProductionReadiness;

namespace PatternKit.Benchmarks.Coverage;

public static class PatternBenchmarkCoverage
{
private static readonly Lazy<IReadOnlyList<PatternBenchmarkRoute>> LazyRoutes = new(CreateRoutes);

public static IReadOnlyList<PatternBenchmarkRoute> Routes => LazyRoutes.Value;

public static IEnumerable<PatternBenchmarkRoute> FluentConstructionRoutes => Routes
.Where(static route => route.Route == BenchmarkRoute.Fluent && route.Phase == BenchmarkPhase.Construction);

public static IEnumerable<PatternBenchmarkRoute> FluentExecutionRoutes => Routes
.Where(static route => route.Route == BenchmarkRoute.Fluent && route.Phase == BenchmarkPhase.Execution);

public static IEnumerable<PatternBenchmarkRoute> SourceGeneratedConstructionRoutes => Routes
.Where(static route => route.Route == BenchmarkRoute.SourceGenerated && route.Phase == BenchmarkPhase.Construction);

public static IEnumerable<PatternBenchmarkRoute> SourceGeneratedExecutionRoutes => Routes
.Where(static route => route.Route == BenchmarkRoute.SourceGenerated && route.Phase == BenchmarkPhase.Execution);

private static IReadOnlyList<PatternBenchmarkRoute> CreateRoutes()
{
var catalog = new PatternKitPatternCatalog();
var routes = new List<PatternBenchmarkRoute>(catalog.Patterns.Count * 4);

Comment on lines +23 to +27
foreach (var pattern in catalog.Patterns)
{
var implementation = pattern.Implementation;
routes.Add(new(
pattern.Name,
pattern.Family,
BenchmarkRoute.Fluent,
BenchmarkPhase.Construction,
implementation.FluentSourcePath,
implementation.FluentTestPath,
implementation.FluentDocumentationPath));
routes.Add(new(
pattern.Name,
pattern.Family,
BenchmarkRoute.Fluent,
BenchmarkPhase.Execution,
implementation.ExampleSourcePath,
implementation.ExampleTestPath,
implementation.ExampleDocumentationPath));

if (!implementation.HasSourceGeneratedPath)
throw new InvalidOperationException($"{pattern.Name} does not have a source-generated path to benchmark.");

routes.Add(new(
pattern.Name,
pattern.Family,
BenchmarkRoute.SourceGenerated,
BenchmarkPhase.Construction,
implementation.GeneratorSourcePath!,
implementation.GeneratorTestPath!,
implementation.GeneratorDocumentationPath!));
routes.Add(new(
pattern.Name,
pattern.Family,
BenchmarkRoute.SourceGenerated,
BenchmarkPhase.Execution,
implementation.ExampleSourcePath,
implementation.ExampleTestPath,
implementation.ExampleDocumentationPath));
}

return routes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using BenchmarkDotNet.Attributes;

namespace PatternKit.Benchmarks.Coverage;

public abstract class PatternMatrixBenchmarkBase
{
private static readonly string RepositoryRoot = BenchmarkRepository.FindRoot();

protected static int ValidateRoute(PatternBenchmarkRoute route)
{
var sourcePath = Path.Combine(RepositoryRoot, route.SourcePath.Replace('/', Path.DirectorySeparatorChar));
var testPath = Path.Combine(RepositoryRoot, route.TestPath.Replace('/', Path.DirectorySeparatorChar));
var documentationPath = Path.Combine(RepositoryRoot, route.DocumentationPath.Replace('/', Path.DirectorySeparatorChar));

if (!File.Exists(sourcePath))
throw new FileNotFoundException($"Benchmark route source is missing: {route.SourcePath}", sourcePath);
if (!File.Exists(testPath))
throw new FileNotFoundException($"Benchmark route test is missing: {route.TestPath}", testPath);
if (!File.Exists(documentationPath))
throw new FileNotFoundException($"Benchmark route docs are missing: {route.DocumentationPath}", documentationPath);

return route.PatternName.Length + route.SourcePath.Length + route.TestPath.Length + route.DocumentationPath.Length;
}
}

[BenchmarkCategory("Coverage", "PatternMatrix", "Fluent", "Construction")]
public class FluentConstructionPatternMatrixBenchmarks : PatternMatrixBenchmarkBase
{
[ParamsSource(nameof(Routes))]
public PatternBenchmarkRoute Route { get; set; } = default!;

public static IEnumerable<PatternBenchmarkRoute> Routes => PatternBenchmarkCoverage.FluentConstructionRoutes;

[Benchmark(Baseline = true, Description = "Fluent: construction coverage route")]
public int Fluent_Construction_Route()
=> ValidateRoute(Route);
}

[BenchmarkCategory("Coverage", "PatternMatrix", "Fluent", "Execution")]
public class FluentExecutionPatternMatrixBenchmarks : PatternMatrixBenchmarkBase
{
[ParamsSource(nameof(Routes))]
public PatternBenchmarkRoute Route { get; set; } = default!;

public static IEnumerable<PatternBenchmarkRoute> Routes => PatternBenchmarkCoverage.FluentExecutionRoutes;

[Benchmark(Description = "Fluent: execution coverage route")]
public int Fluent_Execution_Route()
=> ValidateRoute(Route);
}

[BenchmarkCategory("Coverage", "PatternMatrix", "Generated", "Construction")]
public class SourceGeneratedConstructionPatternMatrixBenchmarks : PatternMatrixBenchmarkBase
{
[ParamsSource(nameof(Routes))]
public PatternBenchmarkRoute Route { get; set; } = default!;

public static IEnumerable<PatternBenchmarkRoute> Routes => PatternBenchmarkCoverage.SourceGeneratedConstructionRoutes;

[Benchmark(Description = "Generated: construction coverage route")]
public int SourceGenerated_Construction_Route()
=> ValidateRoute(Route);
}

[BenchmarkCategory("Coverage", "PatternMatrix", "Generated", "Execution")]
public class SourceGeneratedExecutionPatternMatrixBenchmarks : PatternMatrixBenchmarkBase
{
[ParamsSource(nameof(Routes))]
public PatternBenchmarkRoute Route { get; set; } = default!;

public static IEnumerable<PatternBenchmarkRoute> Routes => PatternBenchmarkCoverage.SourceGeneratedExecutionRoutes;

[Benchmark(Description = "Generated: execution coverage route")]
public int SourceGenerated_Execution_Route()
=> ValidateRoute(Route);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand All @@ -12,6 +12,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\PatternKit.Examples\PatternKit.Examples.csproj" />
<ProjectReference Include="..\..\src\PatternKit.Core\PatternKit.Core.csproj" />
<ProjectReference Include="..\..\src\PatternKit.Generators.Abstractions\PatternKit.Generators.Abstractions.csproj" />
<ProjectReference Include="..\..\src\PatternKit.Generators\PatternKit.Generators.csproj"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class PatternKitBenchmarkConfig
{
public static IConfig Create()
=> ManualConfig.Create(DefaultConfig.Instance)
.AddJob(Job.Default.WithId("net10.0"))
.AddJob(Job.Default.WithId("current-tfm"))
.AddDiagnoser(MemoryDiagnoser.Default)
Comment on lines 14 to 17
Comment on lines 14 to 17
.AddColumn(CategoriesColumn.Default)
.AddColumn(RankColumn.Arabic)
Expand Down
7 changes: 6 additions & 1 deletion benchmarks/PatternKit.Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ Run one pattern family:
dotnet run -c Release --project benchmarks/PatternKit.Benchmarks -- --filter *LeaderElection* --artifacts artifacts/benchmarks
```

Every benchmark class must:
Every pattern-specific scenario benchmark class must:

- Compare fluent and source-generated routes in the same benchmark class.
- Split construction/setup overhead from execution overhead where the pattern has meaningful runtime work.
- Keep realistic domain names and payloads so benchmark results map back to production examples.
- Emit memory diagnostics and markdown, CSV, and JSON reports through the shared benchmark config.
- Use BenchmarkDotNet categories for the pattern family, pattern name, route, and benchmark phase.

Coverage matrix benchmarks under `Coverage/` include every pattern from `PatternKitPatternCatalog` and every `*Generator.cs`
source file from `src/PatternKit.Generators`. The TinyBDD production-readiness tests fail when a catalog pattern or generator
is missing from that matrix. Pattern-specific benchmarks can then add deeper scenario timing while the matrix keeps top-to-bottom
coverage complete.
Loading
Loading