From d1a5089e0efca9059bc7d36142964fcbe7823ddf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 21:49:59 +0000 Subject: [PATCH 1/3] Add BenchmarkDotNet pipeline with GitHub Actions summary Expand the benchmark project into structured scenarios (single, batch, async, presets, DI-vs-direct, and a v3 batch-vs-loop comparison), all with memory diagnostics. Multi-target net8.0/net10.0 and emit GitHub markdown reports via the default exporter. Add a Benchmarks workflow that runs on push/dispatch, publishes the report tables to the job summary, and uploads the raw results. The planned v2-vs-v3 comparison via a second PasswordGenerator package reference is omitted: v2 and v3 both produce PasswordGenerator.dll and collide in one bin folder (extern alias fails with CS0430), so the comparison instead measures v3 usage patterns. https://claude.ai/code/session_01NZJPrT8QFtXsMG6JJ5AJfv --- .github/workflows/benchmarks.yml | 65 +++++++++++++++++++ .../Benchmarks/AsyncBenchmarks.cs | 40 ++++++++++++ .../Benchmarks/BatchGenerationBenchmarks.cs | 33 ++++++++++ .../Benchmarks/InstantiationBenchmarks.cs | 40 ++++++++++++ .../Benchmarks/PresetBenchmarks.cs | 45 +++++++++++++ .../Benchmarks/SingleGenerationBenchmarks.cs | 32 +++++++++ .../Benchmarks/VersionComparisonBenchmarks.cs | 54 +++++++++++++++ .../PasswordBenchmarks.cs | 29 --------- .../PasswordGenerator.Benchmarks.csproj | 3 +- PasswordGenerator.Benchmarks/Program.cs | 15 ++++- 10 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/benchmarks.yml create mode 100644 PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs create mode 100644 PasswordGenerator.Benchmarks/Benchmarks/BatchGenerationBenchmarks.cs create mode 100644 PasswordGenerator.Benchmarks/Benchmarks/InstantiationBenchmarks.cs create mode 100644 PasswordGenerator.Benchmarks/Benchmarks/PresetBenchmarks.cs create mode 100644 PasswordGenerator.Benchmarks/Benchmarks/SingleGenerationBenchmarks.cs create mode 100644 PasswordGenerator.Benchmarks/Benchmarks/VersionComparisonBenchmarks.cs delete mode 100644 PasswordGenerator.Benchmarks/PasswordBenchmarks.cs diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 0000000..527e5b2 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,65 @@ +name: Benchmarks + +on: + push: + branches: [main, dev/v3] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + benchmark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Restore + run: dotnet restore PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj + + - name: Build (Release) + run: dotnet build PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj -c Release --no-restore + + - name: Run benchmarks + run: | + cd PasswordGenerator.Benchmarks + dotnet run -c Release --no-build -f net8.0 -- --filter '*' + + - name: Publish results to workflow summary + if: always() + run: | + echo "# 📊 PasswordGenerator Benchmark Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Run date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + shopt -s nullglob + reports=(PasswordGenerator.Benchmarks/BenchmarkDotNet.Artifacts/results/*-report-github.md) + if [ ${#reports[@]} -eq 0 ]; then + echo "_No benchmark reports were produced._" >> $GITHUB_STEP_SUMMARY + fi + for f in "${reports[@]}"; do + name=$(basename "$f" -report-github.md) + echo "## $name" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + cat "$f" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + done + + - name: Upload raw artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: benchmark-results-${{ github.run_number }} + path: PasswordGenerator.Benchmarks/BenchmarkDotNet.Artifacts/results/ + retention-days: 90 diff --git a/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs new file mode 100644 index 0000000..a0ab1ef --- /dev/null +++ b/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace PasswordGenerator.Benchmarks +{ + /// Cost of the async generation APIs relative to their synchronous counterparts. + [MemoryDiagnoser] + public class AsyncBenchmarks + { + private Password _password = null!; + + [Params(1, 10, 100, 1000, 10000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + _password = new Password(); + } + + [GlobalCleanup] + public void Cleanup() + { + _password.Dispose(); + } + + [Benchmark] + public Task NextAsync() + { + return _password.NextAsync(); + } + + [Benchmark] + public Task> GenerateAsync() + { + return _password.GenerateAsync(Count); + } + } +} diff --git a/PasswordGenerator.Benchmarks/Benchmarks/BatchGenerationBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/BatchGenerationBenchmarks.cs new file mode 100644 index 0000000..33c90b8 --- /dev/null +++ b/PasswordGenerator.Benchmarks/Benchmarks/BatchGenerationBenchmarks.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace PasswordGenerator.Benchmarks +{ + /// Cost of generating a batch of passwords in one call. + [MemoryDiagnoser] + public class BatchGenerationBenchmarks + { + private Password _password = null!; + + [Params(1, 10, 100, 1000, 10000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + _password = new Password(); + } + + [GlobalCleanup] + public void Cleanup() + { + _password.Dispose(); + } + + [Benchmark] + public IReadOnlyList Generate() + { + return _password.Generate(Count); + } + } +} diff --git a/PasswordGenerator.Benchmarks/Benchmarks/InstantiationBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/InstantiationBenchmarks.cs new file mode 100644 index 0000000..988c6eb --- /dev/null +++ b/PasswordGenerator.Benchmarks/Benchmarks/InstantiationBenchmarks.cs @@ -0,0 +1,40 @@ +using System; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; + +namespace PasswordGenerator.Benchmarks +{ + /// Overhead of resolving a generator from the DI container versus constructing one directly. + [MemoryDiagnoser] + public class InstantiationBenchmarks + { + private ServiceProvider _provider = null!; + + [GlobalSetup] + public void Setup() + { + _provider = new ServiceCollection() + .AddPasswordGenerator() + .BuildServiceProvider(); + } + + [GlobalCleanup] + public void Cleanup() + { + _provider.Dispose(); + } + + [Benchmark(Baseline = true)] + public string DirectInstantiation() + { + using var password = new Password(); + return password.Next(); + } + + [Benchmark] + public string ResolveFromContainer() + { + return _provider.GetRequiredService().Next(); + } + } +} diff --git a/PasswordGenerator.Benchmarks/Benchmarks/PresetBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/PresetBenchmarks.cs new file mode 100644 index 0000000..93fc35f --- /dev/null +++ b/PasswordGenerator.Benchmarks/Benchmarks/PresetBenchmarks.cs @@ -0,0 +1,45 @@ +using BenchmarkDotNet.Attributes; + +namespace PasswordGenerator.Benchmarks +{ + /// Cost of generating from each built-in preset. + [MemoryDiagnoser] + public class PresetBenchmarks + { + private IPassword _owasp = null!; + private IPassword _nist = null!; + private IPassword _otp = null!; + private IPassword _apiKey = null!; + private IPassword _environmentName = null!; + private IPasswordGenerator _passphrase = null!; + + [GlobalSetup] + public void Setup() + { + _owasp = Password.ForOwasp(); + _nist = Password.ForNist(); + _otp = Password.ForOtp(); + _apiKey = Password.ForApiKey(); + _environmentName = Password.ForEnvironmentName(); + _passphrase = Password.ForPassphrase(); + } + + [Benchmark] + public string ForOwasp() => _owasp.Next(); + + [Benchmark] + public string ForNist() => _nist.Next(); + + [Benchmark] + public string ForOtp() => _otp.Next(); + + [Benchmark] + public string ForApiKey() => _apiKey.Next(); + + [Benchmark] + public string ForEnvironmentName() => _environmentName.Next(); + + [Benchmark] + public string ForPassphrase() => _passphrase.Next(); + } +} diff --git a/PasswordGenerator.Benchmarks/Benchmarks/SingleGenerationBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/SingleGenerationBenchmarks.cs new file mode 100644 index 0000000..0d9b76f --- /dev/null +++ b/PasswordGenerator.Benchmarks/Benchmarks/SingleGenerationBenchmarks.cs @@ -0,0 +1,32 @@ +using BenchmarkDotNet.Attributes; + +namespace PasswordGenerator.Benchmarks +{ + /// Cost of generating a single password across a range of lengths. + [MemoryDiagnoser] + public class SingleGenerationBenchmarks + { + private Password _password = null!; + + [Params(8, 16, 32, 64, 128)] + public int Length { get; set; } + + [GlobalSetup] + public void Setup() + { + _password = new Password(Length); + } + + [GlobalCleanup] + public void Cleanup() + { + _password.Dispose(); + } + + [Benchmark] + public string Next() + { + return _password.Next(); + } + } +} diff --git a/PasswordGenerator.Benchmarks/Benchmarks/VersionComparisonBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/VersionComparisonBenchmarks.cs new file mode 100644 index 0000000..958bcfd --- /dev/null +++ b/PasswordGenerator.Benchmarks/Benchmarks/VersionComparisonBenchmarks.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; + +namespace PasswordGenerator.Benchmarks +{ + /// + /// Compares ways of producing N passwords in v3. The naive loop-over- + /// pattern (how v2 callers typically batched) is the baseline; the v3 batch + /// API is measured against it. + /// + /// + /// A true v2-vs-v3 comparison cannot run in one assembly: the published v2 package and the v3 + /// project both produce PasswordGenerator.dll, so they collide in a single bin folder. + /// + [MemoryDiagnoser] + public class VersionComparisonBenchmarks + { + private Password _password = null!; + + [Params(1, 10, 100, 1000, 10000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + _password = new Password(); + } + + [GlobalCleanup] + public void Cleanup() + { + _password.Dispose(); + } + + [Benchmark(Baseline = true)] + public int LoopNext() + { + var generated = 0; + for (var i = 0; i < Count; i++) + { + _ = _password.Next(); + generated++; + } + + return generated; + } + + [Benchmark] + public IReadOnlyList BatchGenerate() + { + return _password.Generate(Count); + } + } +} diff --git a/PasswordGenerator.Benchmarks/PasswordBenchmarks.cs b/PasswordGenerator.Benchmarks/PasswordBenchmarks.cs deleted file mode 100644 index 2ee5553..0000000 --- a/PasswordGenerator.Benchmarks/PasswordBenchmarks.cs +++ /dev/null @@ -1,29 +0,0 @@ -using BenchmarkDotNet.Attributes; -using PasswordGenerator; - -namespace PasswordGenerator.Benchmarks -{ - [MemoryDiagnoser] - public class PasswordBenchmarks - { - [Params(1, 100, 1000, 10000)] - public int Count; - - [Benchmark] - public string SingleNext() - { - var pwd = new Password(); - return pwd.Next(); - } - - [Benchmark] - public int Batch() - { - var pwd = new Password(); - var generated = 0; - foreach (var _ in pwd.NextGroup(Count)) - generated++; - return generated; - } - } -} diff --git a/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj b/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj index 6600890..8966802 100644 --- a/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj +++ b/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net8.0;net10.0 enable latest false @@ -10,6 +10,7 @@ + diff --git a/PasswordGenerator.Benchmarks/Program.cs b/PasswordGenerator.Benchmarks/Program.cs index cee61a4..0b5d534 100644 --- a/PasswordGenerator.Benchmarks/Program.cs +++ b/PasswordGenerator.Benchmarks/Program.cs @@ -1,10 +1,21 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Running; namespace PasswordGenerator.Benchmarks { public static class Program { - public static void Main(string[] args) => - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + public static void Main(string[] args) + { + // DefaultConfig already supplies the GitHub markdown exporter (MarkdownExporter-github), + // which produces the *-report-github.md files the workflow drops into the step summary. + var config = DefaultConfig.Instance + .AddDiagnoser(MemoryDiagnoser.Default); + + BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args, config); + } } } From 280d2a37e30b128b6b01089ddcc172952e68c58f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 21:58:09 +0000 Subject: [PATCH 2/3] Target net10.0 and benchmark across both runtimes Add net10.0 to the package's target frameworks (the existing NET8_0_OR_GREATER conditionals cover it) and to the CI/release SDK setup so the new target builds and packs. Upgrade BenchmarkDotNet to 0.15.8 (0.14.0 has no .NET 10 moniker) and add .NET 8 and .NET 10 runtime jobs so every benchmark runs on both, with a Runtime column comparing them side by side in one report. https://claude.ai/code/session_01NZJPrT8QFtXsMG6JJ5AJfv --- .github/workflows/ci.yml | 4 +++- .github/workflows/release.yml | 4 +++- .../PasswordGenerator.Benchmarks.csproj | 2 +- PasswordGenerator.Benchmarks/Program.cs | 8 +++++++- PasswordGenerator/PasswordGenerator.csproj | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe64105..0e291c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: | + 8.0.x + 10.0.x - name: Restore run: dotnet restore PasswordGenerator.sln diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b6fc743..e392bd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: | + 8.0.x + 10.0.x # The release tag drives the published package version (e.g. tag "v3.0.1" -> 3.0.1). - name: Derive version from release tag diff --git a/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj b/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj index 8966802..f445b44 100644 --- a/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj +++ b/PasswordGenerator.Benchmarks/PasswordGenerator.Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/PasswordGenerator.Benchmarks/Program.cs b/PasswordGenerator.Benchmarks/Program.cs index 0b5d534..d28536e 100644 --- a/PasswordGenerator.Benchmarks/Program.cs +++ b/PasswordGenerator.Benchmarks/Program.cs @@ -1,5 +1,7 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; namespace PasswordGenerator.Benchmarks @@ -10,8 +12,12 @@ public static void Main(string[] args) { // DefaultConfig already supplies the GitHub markdown exporter (MarkdownExporter-github), // which produces the *-report-github.md files the workflow drops into the step summary. + // Every benchmark is run on both runtimes so the reports compare .NET 8 against .NET 10 + // side by side (BenchmarkDotNet adds a "Runtime" column). var config = DefaultConfig.Instance - .AddDiagnoser(MemoryDiagnoser.Default); + .AddDiagnoser(MemoryDiagnoser.Default) + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core80)) + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core10_0)); BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) diff --git a/PasswordGenerator/PasswordGenerator.csproj b/PasswordGenerator/PasswordGenerator.csproj index b2f9420..933fdf5 100644 --- a/PasswordGenerator/PasswordGenerator.csproj +++ b/PasswordGenerator/PasswordGenerator.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net8.0 + netstandard2.0;net8.0;net10.0 enable latest From a02f6a5fef6b8dcc606decf4fcdfed7d0a9ae045 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 22:09:47 +0000 Subject: [PATCH 3/3] Add v3.0.0 benchmark results for .NET 8 and .NET 10 Record a sample BenchmarkDotNet run (both runtimes) under benchmarks/ as a readable historical reference. Reduced-sampling numbers; the workflow publishes full-precision results to the run summary. https://claude.ai/code/session_01NZJPrT8QFtXsMG6JJ5AJfv --- benchmarks/v3.0.0.md | 143 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 benchmarks/v3.0.0.md diff --git a/benchmarks/v3.0.0.md b/benchmarks/v3.0.0.md new file mode 100644 index 0000000..135c43d --- /dev/null +++ b/benchmarks/v3.0.0.md @@ -0,0 +1,143 @@ +# PasswordGenerator v3.0.0 — Benchmark Results + +Generated with [BenchmarkDotNet](https://benchmarkdotnet.org/) from +`PasswordGenerator.Benchmarks`, running every scenario on both **.NET 8.0** and +**.NET 10.0** in a single process. + +## Environment + +``` +BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) +Intel Xeon Processor 2.80GHz, 1 CPU, 4 logical and 4 physical cores +[Host] : .NET 8.0.27, X64 RyuJIT x86-64-v4 +.NET 8 : .NET 8.0.27 (8.0.2726.22922) +.NET 10 : .NET 10.0.8 (10.0.826.23019) +``` + +> **Note on precision:** this run used reduced sampling +> (`WarmupCount=1 IterationCount=3 LaunchCount=1`) to keep wall-time short, so +> the error margins are wide — treat the means as indicative rather than +> publication-grade. The `Benchmarks` GitHub Actions workflow runs the full +> default job for higher-precision numbers and publishes them to the run summary. + +## SingleGenerationBenchmarks — `Next()` by password length + +| Method | Runtime | Length | Mean | Allocated | +|--------|---------|-------:|-----:|----------:| +| Next | .NET 10.0 | 8 | 25.64 μs | 80 B | +| Next | .NET 8.0 | 8 | 25.81 μs | 80 B | +| Next | .NET 10.0 | 16 | 57.56 μs | 112 B | +| Next | .NET 8.0 | 16 | 56.08 μs | 112 B | +| Next | .NET 10.0 | 32 | 119.35 μs | 176 B | +| Next | .NET 8.0 | 32 | 121.63 μs | 176 B | +| Next | .NET 10.0 | 64 | 252.17 μs | 304 B | +| Next | .NET 8.0 | 64 | 250.51 μs | 304 B | +| Next | .NET 10.0 | 128 | 494.68 μs | 560 B | +| Next | .NET 8.0 | 128 | 486.25 μs | 560 B | + +## BatchGenerationBenchmarks — `Generate(n)` + +| Method | Runtime | Count | Mean | Allocated | +|--------|---------|------:|-----:|----------:| +| Generate | .NET 10.0 | 1 | 56.11 μs | 176 B | +| Generate | .NET 8.0 | 1 | 56.63 μs | 176 B | +| Generate | .NET 10.0 | 10 | 559.63 μs | 1256 B | +| Generate | .NET 8.0 | 10 | 559.66 μs | 1256 B | +| Generate | .NET 10.0 | 100 | 5,623 μs | 12056 B | +| Generate | .NET 8.0 | 100 | 5,732 μs | 12056 B | +| Generate | .NET 10.0 | 1000 | 55,664 μs | 120056 B | +| Generate | .NET 8.0 | 1000 | 55,856 μs | 120056 B | +| Generate | .NET 10.0 | 10000 | 568,403 μs | 1200056 B | +| Generate | .NET 8.0 | 10000 | 554,511 μs | 1200056 B | + +## AsyncBenchmarks — `NextAsync()` / `GenerateAsync(n)` + +`NextAsync` ignores `Count` (it generates one password), so it stays flat ~56 μs. + +| Method | Runtime | Count | Mean | Allocated | +|--------|---------|------:|-----:|----------:| +| NextAsync | .NET 10.0 | 1 | 56.42 μs | 184 B | +| GenerateAsync | .NET 10.0 | 1 | 58.68 μs | 248 B | +| NextAsync | .NET 8.0 | 1 | 56.57 μs | 184 B | +| GenerateAsync | .NET 8.0 | 1 | 57.43 μs | 248 B | +| NextAsync | .NET 10.0 | 10 | 56.58 μs | 184 B | +| GenerateAsync | .NET 10.0 | 10 | 559.01 μs | 1328 B | +| NextAsync | .NET 8.0 | 10 | 56.72 μs | 184 B | +| GenerateAsync | .NET 8.0 | 10 | 569.33 μs | 1328 B | +| NextAsync | .NET 10.0 | 100 | 57.71 μs | 184 B | +| GenerateAsync | .NET 10.0 | 100 | 5,591 μs | 12128 B | +| NextAsync | .NET 8.0 | 100 | 55.70 μs | 184 B | +| GenerateAsync | .NET 8.0 | 100 | 5,707 μs | 12128 B | +| NextAsync | .NET 10.0 | 1000 | 55.90 μs | 184 B | +| GenerateAsync | .NET 10.0 | 1000 | 55,939 μs | 120128 B | +| NextAsync | .NET 8.0 | 1000 | 56.24 μs | 184 B | +| GenerateAsync | .NET 8.0 | 1000 | 56,175 μs | 120128 B | +| NextAsync | .NET 10.0 | 10000 | 55.76 μs | 184 B | +| GenerateAsync | .NET 10.0 | 10000 | 563,644 μs | 1200128 B | +| NextAsync | .NET 8.0 | 10000 | 56.03 μs | 184 B | +| GenerateAsync | .NET 8.0 | 10000 | 560,427 μs | 1200128 B | + +## PresetBenchmarks + +| Method | Runtime | Mean | Allocated | +|--------|---------|-----:|----------:| +| ForOwasp | .NET 10.0 | 50.58 μs | 112 B | +| ForNist | .NET 10.0 | 38.46 μs | 96 B | +| ForOtp | .NET 10.0 | 19.24 μs | 80 B | +| ForApiKey | .NET 10.0 | 88.12 μs | 176 B | +| ForEnvironmentName | .NET 10.0 | 33.24 μs | 560 B | +| ForPassphrase | .NET 10.0 | 8.29 μs | 287 B | +| ForOwasp | .NET 8.0 | 50.19 μs | 112 B | +| ForNist | .NET 8.0 | 38.61 μs | 96 B | +| ForOtp | .NET 8.0 | 19.39 μs | 80 B | +| ForApiKey | .NET 8.0 | 87.03 μs | 176 B | +| ForEnvironmentName | .NET 8.0 | 33.72 μs | 560 B | +| ForPassphrase | .NET 8.0 | 8.54 μs | 287 B | + +## InstantiationBenchmarks — DI vs direct (baseline = `new Password()`) + +DI resolution is essentially free on time and allocates ~9× less, because the +registered generator is a singleton and is reused on each resolve. + +| Method | Runtime | Mean | Ratio | Allocated | Alloc Ratio | +|--------|---------|-----:|------:|----------:|------------:| +| DirectInstantiation | .NET 10.0 | 56.78 μs | 1.00 | 1032 B | 1.00 | +| ResolveFromContainer | .NET 10.0 | 55.92 μs | 0.98 | 112 B | 0.11 | +| DirectInstantiation | .NET 8.0 | 56.38 μs | 1.00 | 1032 B | 1.00 | +| ResolveFromContainer | .NET 8.0 | 56.04 μs | 0.99 | 112 B | 0.11 | + +## VersionComparisonBenchmarks — loop `Next()` (baseline) vs `Generate(n)` + +| Method | Runtime | Count | Mean | Ratio | Allocated | Alloc Ratio | +|--------|---------|------:|-----:|------:|----------:|------------:| +| LoopNext | .NET 10.0 | 1 | 55.73 μs | 1.00 | 112 B | 1.00 | +| BatchGenerate | .NET 10.0 | 1 | 56.48 μs | 1.01 | 176 B | 1.57 | +| LoopNext | .NET 8.0 | 1 | 56.26 μs | 1.00 | 112 B | 1.00 | +| BatchGenerate | .NET 8.0 | 1 | 56.28 μs | 1.00 | 176 B | 1.57 | +| LoopNext | .NET 10.0 | 10 | 566.68 μs | 1.00 | 1120 B | 1.00 | +| BatchGenerate | .NET 10.0 | 10 | 562.27 μs | 0.99 | 1256 B | 1.12 | +| LoopNext | .NET 8.0 | 10 | 561.85 μs | 1.00 | 1120 B | 1.00 | +| BatchGenerate | .NET 8.0 | 10 | 562.08 μs | 1.00 | 1256 B | 1.12 | +| LoopNext | .NET 10.0 | 100 | 5,498 μs | 1.00 | 11200 B | 1.00 | +| BatchGenerate | .NET 10.0 | 100 | 5,566 μs | 1.01 | 12056 B | 1.08 | +| LoopNext | .NET 8.0 | 100 | 5,543 μs | 1.00 | 11200 B | 1.00 | +| BatchGenerate | .NET 8.0 | 100 | 5,576 μs | 1.01 | 12056 B | 1.08 | +| LoopNext | .NET 10.0 | 1000 | 56,380 μs | 1.00 | 112000 B | 1.00 | +| BatchGenerate | .NET 10.0 | 1000 | 56,404 μs | 1.00 | 120056 B | 1.07 | +| LoopNext | .NET 8.0 | 1000 | 56,727 μs | 1.00 | 112000 B | 1.00 | +| BatchGenerate | .NET 8.0 | 1000 | 56,752 μs | 1.00 | 120056 B | 1.07 | +| LoopNext | .NET 10.0 | 10000 | 555,818 μs | 1.00 | 1120000 B | 1.00 | +| BatchGenerate | .NET 10.0 | 10000 | 551,998 μs | 0.99 | 1200056 B | 1.07 | +| LoopNext | .NET 8.0 | 10000 | 555,037 μs | 1.00 | 1120000 B | 1.00 | +| BatchGenerate | .NET 8.0 | 10000 | 561,637 μs | 1.01 | 1200056 B | 1.07 | + +## Takeaways + +- **.NET 8 vs .NET 10:** within noise across every scenario — runtime is + dominated by `RandomNumberGenerator` crypto calls, not managed code, so the + newer JIT makes little difference here. +- **`Generate(n)` vs a manual `Next()` loop:** identical timing; the batch path + only adds the result `List` allocation (~7% more memory). +- **Cost scales linearly** with password length and batch count, as expected. +- **DI has no measurable overhead** and allocates far less per call than + constructing a new `Password` each time.