diff --git a/README.md b/README.md index 4df829ff..7f6a0c4c 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/benchmarks/PatternKit.Benchmarks/Cloud/AmbassadorBenchmarks.cs b/benchmarks/PatternKit.Benchmarks/Cloud/AmbassadorBenchmarks.cs new file mode 100644 index 00000000..5edc4854 --- /dev/null +++ b/benchmarks/PatternKit.Benchmarks/Cloud/AmbassadorBenchmarks.cs @@ -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 _fluent = + InventoryAmbassadors.CreateFluent(Client); + private readonly Ambassador _generated = + GeneratedInventoryAmbassador.Create(); + + [Benchmark(Baseline = true, Description = "Fluent: create ambassador")] + [BenchmarkCategory("Fluent", "Construction")] + public Ambassador Fluent_CreateAmbassador() + => InventoryAmbassadors.CreateFluent(Client); + + [Benchmark(Description = "Generated: create ambassador")] + [BenchmarkCategory("Generated", "Construction")] + public Ambassador Generated_CreateAmbassador() + => GeneratedInventoryAmbassador.Create(); + + [Benchmark(Description = "Fluent: transform, trace, call")] + [BenchmarkCategory("Fluent", "Execution")] + public AmbassadorResult Fluent_Invoke() + => _fluent.Invoke(Request); + + [Benchmark(Description = "Generated: transform, trace, call")] + [BenchmarkCategory("Generated", "Execution")] + public AmbassadorResult Generated_Invoke() + => _generated.Invoke(Request); +} diff --git a/benchmarks/PatternKit.Benchmarks/Cloud/CacheAsideBenchmarks.cs b/benchmarks/PatternKit.Benchmarks/Cloud/CacheAsideBenchmarks.cs new file mode 100644 index 00000000..7891584c --- /dev/null +++ b/benchmarks/PatternKit.Benchmarks/Cloud/CacheAsideBenchmarks.cs @@ -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 Fluent_CreatePolicy() + => ProductCatalogCacheAsidePolicies.CreateFluentPolicy(); + + [Benchmark(Description = "Generated: create cache-aside policy")] + [BenchmarkCategory("Generated", "Construction")] + public CacheAsidePolicy Generated_CreatePolicy() + => GeneratedProductCatalogCacheAsidePolicy.CreateGeneratedPolicy(); + + [Benchmark(Description = "Fluent: miss then cache hit")] + [BenchmarkCategory("Fluent", "Execution")] + public async ValueTask Fluent_MissThenHit() + { + var service = new ProductCatalogCacheAsideService( + new ScriptedProductCatalogRepository(ActiveProduct), + ProductCatalogCacheAsidePolicies.CreateFluentPolicy()); + + _ = await service.FindAsync("SKU-42"); + return await service.FindAsync("SKU-42"); + } + + [Benchmark(Description = "Generated: miss then cache hit")] + [BenchmarkCategory("Generated", "Execution")] + public async ValueTask Generated_MissThenHit() + { + var service = new ProductCatalogCacheAsideService( + new ScriptedProductCatalogRepository(ActiveProduct), + GeneratedProductCatalogCacheAsidePolicy.CreateGeneratedPolicy()); + + _ = await service.FindAsync("SKU-42"); + return await service.FindAsync("SKU-42"); + } +} diff --git a/benchmarks/PatternKit.Benchmarks/Cloud/RetryBenchmarks.cs b/benchmarks/PatternKit.Benchmarks/Cloud/RetryBenchmarks.cs new file mode 100644 index 00000000..36da004f --- /dev/null +++ b/benchmarks/PatternKit.Benchmarks/Cloud/RetryBenchmarks.cs @@ -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 Fluent_CreatePolicy() + => InventoryRetryPolicies.CreateFluentPolicy(); + + [Benchmark(Description = "Generated: create retry policy")] + [BenchmarkCategory("Generated", "Construction")] + public RetryPolicy Generated_CreatePolicy() + => GeneratedInventoryRetryPolicy.CreateGeneratedPolicy(); + + [Benchmark(Description = "Fluent: retry transient result")] + [BenchmarkCategory("Fluent", "Execution")] + public ValueTask Fluent_RetryTransientResult() + { + var service = new InventoryLookupService( + new ScriptedInventoryClient(Transient, Available), + InventoryRetryPolicies.CreateFluentPolicy()); + + return service.CheckAsync("SKU-42"); + } + + [Benchmark(Description = "Generated: retry transient result")] + [BenchmarkCategory("Generated", "Execution")] + public ValueTask Generated_RetryTransientResult() + { + var service = new InventoryLookupService( + new ScriptedInventoryClient(Transient, Available), + GeneratedInventoryRetryPolicy.CreateGeneratedPolicy()); + + return service.CheckAsync("SKU-42"); + } +} diff --git a/docs/guides/benchmark-results.md b/docs/guides/benchmark-results.md index 014a46d2..137ab2b2 100644 --- a/docs/guides/benchmark-results.md +++ b/docs/guides/benchmark-results.md @@ -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 diff --git a/docs/guides/benchmarks.md b/docs/guides/benchmarks.md index 436d3598..ca4f94cc 100644 --- a/docs/guides/benchmarks.md +++ b/docs/guides/benchmarks.md @@ -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. | diff --git a/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitBenchmarkCoverageTests.cs b/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitBenchmarkCoverageTests.cs index d5a8ba92..469d73db 100644 --- a/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitBenchmarkCoverageTests.cs +++ b/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitBenchmarkCoverageTests.cs @@ -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 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(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);