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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,14 @@ BenchmarkDotNet guidance is documented in [docs/guides/benchmarks.md](docs/guide

| Pattern | Phase | Fluent mean | Fluent allocation | Generated mean | Generated allocation | Read |
| --- | --- | ---: | ---: | ---: | ---: | --- |
| Ambassador | Construction | 55.42 ns | 448 B | 48.03 ns | 360 B | Generated reduced construction time and allocation in this microbenchmark. |
| Ambassador | Execution | 87.92 ns | 624 B | 93.72 ns | 624 B | Same allocation; fluent was slightly faster in this path. |
| Cache-Aside | Construction | 19.91 ns | 200 B | 19.85 ns | 200 B | Effectively equivalent for this microbenchmark. |
| Cache-Aside | Execution | 216.50 ns | 1,048 B | 208.60 ns | 1,048 B | Same allocation; generated was slightly faster for the miss-then-hit workflow. |
| Leader Election | Construction | 14.28 ns | 104 B | 15.91 ns | 104 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Leader Election | Execution | 43.62 ns | 360 B | 144.37 ns | 312 B | Generated allocated about 13% less memory, while fluent was faster in this path. |
| Retry | Construction | 25.36 ns | 208 B | 27.18 ns | 208 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Retry | Execution | 110.53 ns | 600 B | 109.52 ns | 600 B | Same allocation; generated was slightly faster for the transient retry workflow. |
| Scheduler Agent Supervisor | Construction | 47.29 ns | 400 B | 45.40 ns | 400 B | Same allocation; generated was slightly faster in this microbenchmark. |
| Scheduler Agent Supervisor | Execution | 177.46 ns | 1,304 B | 180.14 ns | 1,304 B | Effectively equivalent for this scenario. |

Expand Down
36 changes: 36 additions & 0 deletions benchmarks/PatternKit.Benchmarks/Cloud/AmbassadorBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Cloud.Ambassador;
using PatternKit.Examples.AmbassadorDemo;

namespace PatternKit.Benchmarks.Cloud;

[BenchmarkCategory("Cloud", "Ambassador")]
public class AmbassadorBenchmarks
{
private static readonly InventoryAmbassadorRequest Request = new("sku-42", "tenant-a");
private static readonly DemoInventoryAvailabilityClient Client = new();
private readonly Ambassador<InventoryAmbassadorRequest, InventoryAmbassadorResponse> _fluent =
InventoryAmbassadors.CreateFluent(Client);
private readonly Ambassador<InventoryAmbassadorRequest, InventoryAmbassadorResponse> _generated =
GeneratedInventoryAmbassador.Create();

[Benchmark(Baseline = true, Description = "Fluent: create ambassador")]
[BenchmarkCategory("Fluent", "Construction")]
public Ambassador<InventoryAmbassadorRequest, InventoryAmbassadorResponse> Fluent_CreateAmbassador()
=> InventoryAmbassadors.CreateFluent(Client);

[Benchmark(Description = "Generated: create ambassador")]
[BenchmarkCategory("Generated", "Construction")]
public Ambassador<InventoryAmbassadorRequest, InventoryAmbassadorResponse> Generated_CreateAmbassador()
=> GeneratedInventoryAmbassador.Create();

[Benchmark(Description = "Fluent: transform, trace, call")]
[BenchmarkCategory("Fluent", "Execution")]
public AmbassadorResult<InventoryAmbassadorResponse> Fluent_Invoke()
=> _fluent.Invoke(Request);

[Benchmark(Description = "Generated: transform, trace, call")]
[BenchmarkCategory("Generated", "Execution")]
public AmbassadorResult<InventoryAmbassadorResponse> Generated_Invoke()
=> _generated.Invoke(Request);
}
45 changes: 45 additions & 0 deletions benchmarks/PatternKit.Benchmarks/Cloud/CacheAsideBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Cloud.CacheAside;
using PatternKit.Examples.CacheAsideDemo;

namespace PatternKit.Benchmarks.Cloud;

[BenchmarkCategory("Cloud", "CacheAside")]
public class CacheAsideBenchmarks
{
private static readonly ProductReadModel ActiveProduct = new("SKU-42", "Trail Jacket", 129m, true);

[Benchmark(Baseline = true, Description = "Fluent: create cache-aside policy")]
[BenchmarkCategory("Fluent", "Construction")]
public CacheAsidePolicy<ProductReadModel> Fluent_CreatePolicy()
=> ProductCatalogCacheAsidePolicies.CreateFluentPolicy();

[Benchmark(Description = "Generated: create cache-aside policy")]
[BenchmarkCategory("Generated", "Construction")]
public CacheAsidePolicy<ProductReadModel> Generated_CreatePolicy()
=> GeneratedProductCatalogCacheAsidePolicy.CreateGeneratedPolicy();

[Benchmark(Description = "Fluent: miss then cache hit")]
[BenchmarkCategory("Fluent", "Execution")]
public async ValueTask<ProductCatalogLookup> Fluent_MissThenHit()
{
var service = new ProductCatalogCacheAsideService(
new ScriptedProductCatalogRepository(ActiveProduct),
ProductCatalogCacheAsidePolicies.CreateFluentPolicy());

_ = await service.FindAsync("SKU-42");
return await service.FindAsync("SKU-42");
Comment on lines +26 to +31
}

[Benchmark(Description = "Generated: miss then cache hit")]
[BenchmarkCategory("Generated", "Execution")]
public async ValueTask<ProductCatalogLookup> Generated_MissThenHit()
{
var service = new ProductCatalogCacheAsideService(
new ScriptedProductCatalogRepository(ActiveProduct),
GeneratedProductCatalogCacheAsidePolicy.CreateGeneratedPolicy());

_ = await service.FindAsync("SKU-42");
return await service.FindAsync("SKU-42");
}
}
44 changes: 44 additions & 0 deletions benchmarks/PatternKit.Benchmarks/Cloud/RetryBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using BenchmarkDotNet.Attributes;
using PatternKit.Cloud.Retry;
using PatternKit.Examples.RetryDemo;

namespace PatternKit.Benchmarks.Cloud;

[BenchmarkCategory("Cloud", "Retry")]
public class RetryBenchmarks
{
private static readonly InventoryResponse Transient = new("SKU-42", 0, 503);
private static readonly InventoryResponse Available = new("SKU-42", 12, 200);

[Benchmark(Baseline = true, Description = "Fluent: create retry policy")]
[BenchmarkCategory("Fluent", "Construction")]
public RetryPolicy<InventoryResponse> Fluent_CreatePolicy()
=> InventoryRetryPolicies.CreateFluentPolicy();

[Benchmark(Description = "Generated: create retry policy")]
[BenchmarkCategory("Generated", "Construction")]
public RetryPolicy<InventoryResponse> Generated_CreatePolicy()
=> GeneratedInventoryRetryPolicy.CreateGeneratedPolicy();

[Benchmark(Description = "Fluent: retry transient result")]
[BenchmarkCategory("Fluent", "Execution")]
public ValueTask<InventoryLookupResult> Fluent_RetryTransientResult()
{
var service = new InventoryLookupService(
new ScriptedInventoryClient(Transient, Available),
InventoryRetryPolicies.CreateFluentPolicy());

return service.CheckAsync("SKU-42");
}
Comment on lines +27 to +32

[Benchmark(Description = "Generated: retry transient result")]
[BenchmarkCategory("Generated", "Execution")]
public ValueTask<InventoryLookupResult> Generated_RetryTransientResult()
{
var service = new InventoryLookupService(
new ScriptedInventoryClient(Transient, Available),
GeneratedInventoryRetryPolicy.CreateGeneratedPolicy());

return service.CheckAsync("SKU-42");
}
}
18 changes: 12 additions & 6 deletions docs/guides/benchmark-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ The latest measured timings below were captured on Windows 11, Intel Core i9-149

## Scenario Timing Results

| Pattern | Phase | Fluent mean | Fluent allocation | Generated mean | Generated allocation | Decision signal |
| --- | --- | ---: | ---: | ---: | ---: | --- |
| Leader Election | Construction | 14.28 ns | 104 B | 15.91 ns | 104 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Leader Election | Execution | 43.62 ns | 360 B | 144.37 ns | 312 B | Generated allocated about 13% less memory, while fluent was faster in this path. |
| Scheduler Agent Supervisor | Construction | 47.29 ns | 400 B | 45.40 ns | 400 B | Same allocation; generated was slightly faster in this microbenchmark. |
| Scheduler Agent Supervisor | Execution | 177.46 ns | 1,304 B | 180.14 ns | 1,304 B | Effectively equivalent for this scenario. |
| Pattern | Phase | Fluent mean | Fluent allocation | Generated mean | Generated allocation | Decision signal |
| --- | --- | ---: | ---: | ---: | ---: | --- |
| Ambassador | Construction | 55.42 ns | 448 B | 48.03 ns | 360 B | Generated reduced construction time and allocation in this microbenchmark. |
| Ambassador | Execution | 87.92 ns | 624 B | 93.72 ns | 624 B | Same allocation; fluent was slightly faster in this path. |
| Cache-Aside | Construction | 19.91 ns | 200 B | 19.85 ns | 200 B | Effectively equivalent for this microbenchmark. |
| Cache-Aside | Execution | 216.50 ns | 1,048 B | 208.60 ns | 1,048 B | Same allocation; generated was slightly faster for the miss-then-hit workflow. |
| Leader Election | Construction | 14.28 ns | 104 B | 15.91 ns | 104 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Leader Election | Execution | 43.62 ns | 360 B | 144.37 ns | 312 B | Generated allocated about 13% less memory, while fluent was faster in this path. |
| Retry | Construction | 25.36 ns | 208 B | 27.18 ns | 208 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Retry | Execution | 110.53 ns | 600 B | 109.52 ns | 600 B | Same allocation; generated was slightly faster for the transient retry workflow. |
| Scheduler Agent Supervisor | Construction | 47.29 ns | 400 B | 45.40 ns | 400 B | Same allocation; generated was slightly faster in this microbenchmark. |
| Scheduler Agent Supervisor | Execution | 177.46 ns | 1,304 B | 180.14 ns | 1,304 B | Effectively equivalent for this scenario. |

## Coverage Matrix Summary

Expand Down
6 changes: 6 additions & 0 deletions docs/guides/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ The following numbers were captured on Windows 11, Intel Core i9-14900K, .NET SD

| Pattern | Phase | Fluent mean | Fluent allocation | Generated mean | Generated allocation | Decision signal |
| --- | --- | ---: | ---: | ---: | ---: | --- |
| Ambassador | Construction | 55.42 ns | 448 B | 48.03 ns | 360 B | Generated reduced construction time and allocation in this microbenchmark. |
| Ambassador | Execution | 87.92 ns | 624 B | 93.72 ns | 624 B | Same allocation; fluent was slightly faster in this path. |
| Cache-Aside | Construction | 19.91 ns | 200 B | 19.85 ns | 200 B | Effectively equivalent for this microbenchmark. |
| Cache-Aside | Execution | 216.50 ns | 1,048 B | 208.60 ns | 1,048 B | Same allocation; generated was slightly faster for the miss-then-hit workflow. |
| Leader Election | Construction | 14.28 ns | 104 B | 15.91 ns | 104 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Leader Election | Execution | 43.62 ns | 360 B | 144.37 ns | 312 B | Generated allocated about 13% less memory, while fluent was faster in this path. |
| Retry | Construction | 25.36 ns | 208 B | 27.18 ns | 208 B | Same allocation; fluent was slightly faster in this microbenchmark. |
| Retry | Execution | 110.53 ns | 600 B | 109.52 ns | 600 B | Same allocation; generated was slightly faster for the transient retry workflow. |
| Scheduler Agent Supervisor | Construction | 47.29 ns | 400 B | 45.40 ns | 400 B | Same allocation; generated was slightly faster in this microbenchmark. |
| Scheduler Agent Supervisor | Execution | 177.46 ns | 1,304 B | 180.14 ns | 1,304 B | Effectively equivalent for this scenario. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,70 @@ public Task Published_Benchmark_Results_Include_Every_Generator_Source()
ScenarioExpect.Contains($"{ctx.GeneratorCount} generator source route results", ctx.ResultsGuide))
.AssertPassed();

[Scenario("Published benchmark results include every dedicated scenario benchmark")]
[Fact]
public Task Published_Benchmark_Results_Include_Every_Dedicated_Scenario_Benchmark()
=> Given("the benchmark results guide and dedicated scenario benchmark classes", () =>
{
var repositoryRoot = FindRepoRoot();
return new
{
ResultsGuide = File.ReadAllText(Path.Combine(repositoryRoot, "docs", "guides", "benchmark-results.md")),
ScenarioBenchmarks = Directory
.EnumerateFiles(Path.Combine(repositoryRoot, "benchmarks", "PatternKit.Benchmarks"), "*Benchmarks.cs", SearchOption.AllDirectories)
.Where(static path => !path.Replace('\\', '/').Contains("/Coverage/", StringComparison.Ordinal))
.Select(static path => HumanizeScenarioBenchmarkName(Path.GetFileNameWithoutExtension(path)))
.OrderBy(static name => name)
.ToArray()
};
})
.When("checking scenario benchmark names against the published timing table", ctx => new
{
ctx.ResultsGuide,
MissingRows = ctx.ScenarioBenchmarks
.SelectMany(patternName => new[]
{
ctx.ResultsGuide.Contains($"| {patternName} | Construction |", StringComparison.Ordinal)
? null
: $"{patternName} missing construction timing row",
ctx.ResultsGuide.Contains($"| {patternName} | Execution |", StringComparison.Ordinal)
? null
: $"{patternName} missing execution timing row"
})
.Where(static issue => issue is not null)
.Select(static issue => issue!)
.OrderBy(static issue => issue)
.ToArray()
})
.Then("each dedicated scenario benchmark has construction and execution results", ctx =>
ScenarioExpect.Empty(ctx.MissingRows))
.AssertPassed();

private static bool HasRoute(IEnumerable<PatternBenchmarkRoute> routes, BenchmarkRoute route, BenchmarkPhase phase)
=> routes.Any(candidate => candidate.Route == route && candidate.Phase == phase);

private static string HumanizeScenarioBenchmarkName(string benchmarkClassName)
{
var patternName = benchmarkClassName.EndsWith("Benchmarks", StringComparison.Ordinal)
? benchmarkClassName[..^"Benchmarks".Length]
: benchmarkClassName;

if (patternName == "CacheAside")
return "Cache-Aside";

var chars = new List<char>(patternName.Length + 4);
for (var index = 0; index < patternName.Length; index++)
{
var current = patternName[index];
if (index > 0 && char.IsUpper(current) && char.IsLower(patternName[index - 1]))
chars.Add(' ');

chars.Add(current);
}

return new string(chars.ToArray());
}

private static string FindRepoRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
Expand Down
Loading