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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
Expand Down
3 changes: 3 additions & 0 deletions PatternKit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
<Project Path="src/PatternKit.Generators/PatternKit.Generators.csproj" />
<Project Path="src/PatternKit.Generators.Abstractions/PatternKit.Generators.Abstractions.csproj" />
</Folder>
<Folder Name="/benchmarks/">
<Project Path="benchmarks\PatternKit.Benchmarks\PatternKit.Benchmarks.csproj" Type="Classic C#" />
</Folder>
<Folder Name="/test/">
<Project Path="test/PatternKit.Tests/PatternKit.Tests.csproj" />
<Project Path="test\PatternKit.Examples.Tests\PatternKit.Examples.Tests.csproj" Type="Classic C#" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Cloud.LeaderElection;
using PatternKit.Generators.LeaderElection;

namespace PatternKit.Benchmarks.Cloud;

[BenchmarkCategory("Cloud", "LeaderElection")]
public class LeaderElectionBenchmarks
{
private static readonly DateTimeOffset Now = new(2026, 5, 23, 12, 0, 0, TimeSpan.Zero);
private static readonly TimeSpan LeaseDuration = TimeSpan.FromSeconds(5);
private readonly LeaderElectionBenchmarkContext _context = new("node-a");

[Benchmark(Baseline = true, Description = "Fluent: create election and candidate")]
[BenchmarkCategory("Fluent", "Construction")]
public (LeaderElection<LeaderElectionBenchmarkContext> Election, LeaderElectionCandidate<LeaderElectionBenchmarkContext> Candidate) Fluent_CreateElectionAndCandidate()
{
var election = LeaderElection<LeaderElectionBenchmarkContext>
.Create("benchmark-leader")
.LeaseDuration(LeaseDuration)
.Clock(static () => Now)
.Build();
var candidate = LeaderElectionCandidate
.Create(_context.NodeId, _context)
.OnAcquired(static (lease, context) => context.AcquiredTerm = lease.Term)
.OnRenewed(static (lease, context) => context.RenewedTerm = lease.Term)
.OnReleased(static context => context.ReleasedCount++)
.Build();

return (election, candidate);
}

[Benchmark(Description = "Generated: create election and candidate")]
[BenchmarkCategory("Generated", "Construction")]
public (LeaderElection<LeaderElectionBenchmarkContext> Election, LeaderElectionCandidate<LeaderElectionBenchmarkContext> Candidate) Generated_CreateElectionAndCandidate()
=> (GeneratedLeaderElectionBenchmark.CreateElection(), GeneratedLeaderElectionBenchmark.Create(_context));

[Benchmark(Description = "Fluent: acquire, renew, release")]
[BenchmarkCategory("Fluent", "Execution")]
public LeaderElectionResult Fluent_AcquireRenewRelease()
{
var election = LeaderElection<LeaderElectionBenchmarkContext>
.Create("benchmark-leader")
.LeaseDuration(LeaseDuration)
.Clock(static () => Now)
.Build();
var candidate = Fluent_CreateElectionAndCandidate().Candidate;

_ = election.TryAcquire(candidate);
_ = election.Renew(candidate);
return election.Release(candidate);
}

[Benchmark(Description = "Generated: acquire, renew, release")]
[BenchmarkCategory("Generated", "Execution")]
public LeaderElectionResult Generated_AcquireRenewRelease()
{
var election = GeneratedLeaderElectionBenchmark.CreateElection();
var candidate = GeneratedLeaderElectionBenchmark.Create(_context);
Comment on lines +58 to +59

_ = election.TryAcquire(candidate);
_ = election.Renew(candidate);
return election.Release(candidate);
}
}

public sealed record LeaderElectionBenchmarkContext(string NodeId)
{
public long AcquiredTerm { get; set; }

public long RenewedTerm { get; set; }

public int ReleasedCount { get; set; }
}

[GenerateLeaderElection(
typeof(LeaderElectionBenchmarkContext),
FactoryMethodName = "Create",
ElectionName = "benchmark-leader",
LeaseDurationMilliseconds = 5000)]
public static partial class GeneratedLeaderElectionBenchmark
{
[LeaderCandidateId]
private static string CandidateId(LeaderElectionBenchmarkContext context) => context.NodeId;

[LeaderAcquired]
private static void Acquired(LeaderLease lease, LeaderElectionBenchmarkContext context) => context.AcquiredTerm = lease.Term;

[LeaderRenewed]
private static void Renewed(LeaderLease lease, LeaderElectionBenchmarkContext context) => context.RenewedTerm = lease.Term;

[LeaderReleased]
private static void Released(LeaderElectionBenchmarkContext context) => context.ReleasedCount++;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Cloud.SchedulerAgentSupervisor;
using PatternKit.Generators.SchedulerAgentSupervisor;

namespace PatternKit.Benchmarks.Cloud;

[BenchmarkCategory("Cloud", "SchedulerAgentSupervisor")]
public class SchedulerAgentSupervisorBenchmarks
{
private static readonly DateTimeOffset Now = new(2026, 5, 23, 12, 0, 0, TimeSpan.Zero);
private readonly SchedulerWork _work = new("sku-100", 12);

[Benchmark(Baseline = true, Description = "Fluent: create supervisor")]
[BenchmarkCategory("Fluent", "Construction")]
public SchedulerAgentSupervisor<SchedulerWork, SchedulerSummary> Fluent_CreateSupervisor()
=> SchedulerAgentSupervisor<SchedulerWork, SchedulerSummary>
.Create("benchmark-scheduler")
.Supervision(SchedulerSupervisionPolicy<SchedulerWork>
.Create()
.MaxAttempts(3)
.RetryDelay(TimeSpan.FromMilliseconds(250))
.RetryWhen(static (_, context) => context.Attempt < 3)
.Build())
.Agent("release-agent", static context => new SchedulerSummary(context.Work.Sku, context.Work.Quantity))
.Build();

[Benchmark(Description = "Generated: create supervisor")]
[BenchmarkCategory("Generated", "Construction")]
public SchedulerAgentSupervisor<SchedulerWork, SchedulerSummary> Generated_CreateSupervisor()
=> GeneratedSchedulerAgentSupervisorBenchmark.Create();

[Benchmark(Description = "Fluent: schedule and run due")]
[BenchmarkCategory("Fluent", "Execution")]
public IReadOnlyList<SchedulerAgentResult<SchedulerSummary>> Fluent_ScheduleAndRunDue()
=> Fluent_CreateSupervisor()
.Schedule("replenish", _work, Now)
.RunDue(Now);
Comment on lines +35 to +37

[Benchmark(Description = "Generated: schedule and run due")]
[BenchmarkCategory("Generated", "Execution")]
public IReadOnlyList<SchedulerAgentResult<SchedulerSummary>> Generated_ScheduleAndRunDue()
=> Generated_CreateSupervisor()
.Schedule("replenish", _work, Now)
.RunDue(Now);
Comment on lines +42 to +44
}

public sealed record SchedulerWork(string Sku, int Quantity);

public sealed record SchedulerSummary(string Sku, int Released);

[GenerateSchedulerAgentSupervisor(
typeof(SchedulerWork),
typeof(SchedulerSummary),
FactoryMethodName = "Create",
SupervisorName = "benchmark-scheduler",
MaxAttempts = 3,
RetryDelayMilliseconds = 250)]
public static partial class GeneratedSchedulerAgentSupervisorBenchmark
{
[SchedulerAgent("release-agent")]
private static SchedulerSummary Release(SchedulerAgentContext<SchedulerWork> context)
=> new(context.Work.Sku, context.Work.Quantity);

[SchedulerRetryWhen]
private static bool Retry(Exception exception, SchedulerAgentContext<SchedulerWork> context)
=> context.Attempt < 3;
}
72 changes: 72 additions & 0 deletions benchmarks/PatternKit.Benchmarks/JsonSummaryExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Text.Json;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;

namespace PatternKit.Benchmarks;

public sealed class JsonSummaryExporter : IExporter
{
public static readonly JsonSummaryExporter Default = new();

private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};

private JsonSummaryExporter()
{
}

public string Name => "json-summary";

public void ExportToLog(Summary summary, ILogger logger)
{
}

public IEnumerable<string> ExportToFiles(Summary summary, ILogger consoleLogger)
{
Directory.CreateDirectory(summary.ResultsDirectoryPath);
var path = Path.Combine(summary.ResultsDirectoryPath, $"{Sanitize(summary.Title)}-summary.json");
var payload = new
{
summary.Title,
summary.AllRuntimes,
totalTime = summary.TotalTime,
host = summary.HostEnvironmentInfo.ToString(),
benchmarks = summary.Reports.Select(static report => new
{
name = report.BenchmarkCase.Descriptor.WorkloadMethodDisplayInfo,
displayName = report.BenchmarkCase.DisplayInfo,
job = report.BenchmarkCase.Job.DisplayInfo,
success = report.Success,
statistics = report.ResultStatistics is null
? null
: new
{
report.ResultStatistics.N,
report.ResultStatistics.Mean,
report.ResultStatistics.Median,
report.ResultStatistics.Min,
report.ResultStatistics.Max,
report.ResultStatistics.StandardDeviation,
report.ResultStatistics.StandardError
},
gc = new
{
report.GcStats.TotalOperations,
report.GcStats.Gen0Collections,
report.GcStats.Gen1Collections,
report.GcStats.Gen2Collections
},
metrics = report.Metrics.ToDictionary(static metric => metric.Key, static metric => metric.Value.ToString())
})
};

File.WriteAllText(path, JsonSerializer.Serialize(payload, Options));
return [path];
}

private static string Sanitize(string value)
=> string.Concat(value.Select(static character => Path.GetInvalidFileNameChars().Contains(character) ? '-' : character));
}
22 changes: 22 additions & 0 deletions benchmarks/PatternKit.Benchmarks/PatternKit.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
</ItemGroup>

<ItemGroup>
<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"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
AdditionalProperties="TargetFramework=netstandard2.0" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions benchmarks/PatternKit.Benchmarks/PatternKitBenchmarkConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Exporters;
using BenchmarkDotNet.Exporters.Csv;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Order;

namespace PatternKit.Benchmarks;

public static class PatternKitBenchmarkConfig
{
public static IConfig Create()
=> ManualConfig.Create(DefaultConfig.Instance)
.AddJob(Job.Default.WithId("net10.0"))
.AddDiagnoser(MemoryDiagnoser.Default)
.AddColumn(CategoriesColumn.Default)
.AddColumn(RankColumn.Arabic)
.AddExporter(MarkdownExporter.GitHub)
.AddExporter(CsvExporter.Default)
.AddExporter(JsonSummaryExporter.Default)
.AddLogger(ConsoleLogger.Default)
.WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest))
.WithOptions(ConfigOptions.JoinSummary);
}
6 changes: 6 additions & 0 deletions benchmarks/PatternKit.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using BenchmarkDotNet.Running;
using PatternKit.Benchmarks;

BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
.Run(args, PatternKitBenchmarkConfig.Create());
23 changes: 23 additions & 0 deletions benchmarks/PatternKit.Benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# PatternKit Benchmarks

This project contains reportable BenchmarkDotNet comparisons for PatternKit fluent builders and source-generated factories.

Run the full benchmark suite from the repository root:

```powershell
dotnet run -c Release --project benchmarks/PatternKit.Benchmarks -- --artifacts artifacts/benchmarks
```

Run one pattern family:

```powershell
dotnet run -c Release --project benchmarks/PatternKit.Benchmarks -- --filter *LeaderElection* --artifacts artifacts/benchmarks
```

Every 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.
Loading
Loading