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
65 changes: 65 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;

namespace PasswordGenerator.Benchmarks
{
/// <summary>Cost of the async generation APIs relative to their synchronous counterparts.</summary>
[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<string> NextAsync()
{
return _password.NextAsync();
}

[Benchmark]
public Task<IReadOnlyList<string>> GenerateAsync()
{
return _password.GenerateAsync(Count);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;

namespace PasswordGenerator.Benchmarks
{
/// <summary>Cost of generating a batch of passwords in one call.</summary>
[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<string> Generate()
{
return _password.Generate(Count);
}
}
}
40 changes: 40 additions & 0 deletions PasswordGenerator.Benchmarks/Benchmarks/InstantiationBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;

namespace PasswordGenerator.Benchmarks
{
/// <summary>Overhead of resolving a generator from the DI container versus constructing one directly.</summary>
[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<IPasswordGenerator>().Next();
}
}
}
45 changes: 45 additions & 0 deletions PasswordGenerator.Benchmarks/Benchmarks/PresetBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;

namespace PasswordGenerator.Benchmarks
{
/// <summary>Cost of generating from each built-in preset.</summary>
[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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using BenchmarkDotNet.Attributes;

namespace PasswordGenerator.Benchmarks
{
/// <summary>Cost of generating a single password across a range of lengths.</summary>
[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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;

namespace PasswordGenerator.Benchmarks
{
/// <summary>
/// Compares ways of producing N passwords in v3. The naive loop-over-<see cref="Password.Next" />
/// pattern (how v2 callers typically batched) is the baseline; the v3 batch
/// <see cref="Password.Generate(int)" /> API is measured against it.
/// </summary>
/// <remarks>
/// A true v2-vs-v3 comparison cannot run in one assembly: the published v2 package and the v3
/// project both produce <c>PasswordGenerator.dll</c>, so they collide in a single bin folder.
/// </remarks>
[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<string> BatchGenerate()
{
return _password.Generate(Count);
}
}
}
29 changes: 0 additions & 29 deletions PasswordGenerator.Benchmarks/PasswordBenchmarks.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading