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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
# SourceGenerator Launch Settings
src/CompositeKey.SourceGeneration/Properties/launchSettings.json

# BenchmarkDotNet
BenchmarkDotNet.Artifacts/

# Verify snapshot testing
*.received.*

Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ The solution is split into four main projects plus corresponding test projects:
- **Central Package Management**: via `Directory.Packages.props`
- Analyzer projects target both `net8.0` and `netstandard2.0`

## Benchmarks

**CompositeKey.Benchmarks** (`src/CompositeKey.Benchmarks/`) — BenchmarkDotNet benchmarks for generated code performance. Manual, developer-run (not part of CI). Covers `ToString`, `Parse`, `TryParse`, and partition/sort key methods across representative key types exercising all major code-generation paths.

```bash
dotnet run -c Release --project src/CompositeKey.Benchmarks -- --filter "*"
```

## Conventions

- Commit messages follow Conventional Commits (`feat:`, `fix:`, `perf:`, etc.) — GitVersion and `.versionrc` drive versioning and changelog
Expand Down
3 changes: 3 additions & 0 deletions CompositeKey.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@
<Project Path="src\CompositeKey.SourceGeneration.FunctionalTests\CompositeKey.SourceGeneration.FunctionalTests.csproj" Type="Classic C#" />
<Project Path="src\CompositeKey.SourceGeneration.UnitTests\CompositeKey.SourceGeneration.UnitTests.csproj" Type="Classic C#" />
</Folder>
<Folder Name="/benchmarks/">
<Project Path="src\CompositeKey.Benchmarks\CompositeKey.Benchmarks.csproj" Type="Classic C#" />
</Folder>
</Solution>
21 changes: 21 additions & 0 deletions src/CompositeKey.Benchmarks/CompositeKey.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>

<IsPackable>false</IsPackable>
</PropertyGroup>

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

<ItemGroup>
<ProjectReference Include="..\CompositeKey\CompositeKey.csproj" />
<ProjectReference Include="..\CompositeKey.Analyzers.Common\CompositeKey.Analyzers.Common.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" GlobalPropertiesToRemove="TargetFramework" />
<ProjectReference Include="..\CompositeKey.SourceGeneration\CompositeKey.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions src/CompositeKey.Benchmarks/FastPathEnumKeyBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using BenchmarkDotNet.Attributes;

namespace CompositeKey.Benchmarks;

/// <summary>
/// Fast-path enum key — exercises the generated enum helper fast path.
/// </summary>
[CompositeKey("{GuidValue}#Constant#{EnumValue}@{StringValue}")]
public sealed partial record FastPathEnumKey(Guid GuidValue, FastPathEnumKey.KeyEnumType EnumValue, string StringValue)
{
public enum KeyEnumType { One, Two, Three, Four, Five, Six, Seven, Eight, Nine }
}

[MemoryDiagnoser]
public class FastPathEnumKeyBenchmarks
{
private FastPathEnumKey _key = null!;
private string _formatted = null!;

[GlobalSetup]
public void Setup()
{
_key = new FastPathEnumKey(Guid.NewGuid(), FastPathEnumKey.KeyEnumType.Five, "test-value");
_formatted = _key.ToString();
}

[Benchmark]
public string ToString_FastPathEnumKey() => _key.ToString();

[Benchmark]
public FastPathEnumKey Parse_FastPathEnumKey() => FastPathEnumKey.Parse(_formatted);

[Benchmark]
public bool TryParse_FastPathEnumKey() => FastPathEnumKey.TryParse(_formatted, out _);
}
32 changes: 32 additions & 0 deletions src/CompositeKey.Benchmarks/GuidPrimaryKeyBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using BenchmarkDotNet.Attributes;

namespace CompositeKey.Benchmarks;

/// <summary>
/// Simple Guid primary key — exercises stackalloc split and Guid.TryParseExact parse path.
/// </summary>
[CompositeKey("{First}#{Second}")]
public sealed partial record GuidPrimaryKey(Guid First, Guid Second);

[MemoryDiagnoser]
public class GuidPrimaryKeyBenchmarks
{
private GuidPrimaryKey _key = null!;
private string _formatted = null!;

[GlobalSetup]
public void Setup()
{
_key = new GuidPrimaryKey(Guid.NewGuid(), Guid.NewGuid());
_formatted = _key.ToString();
}

[Benchmark]
public string ToString_GuidPrimaryKey() => _key.ToString();

[Benchmark]
public GuidPrimaryKey Parse_GuidPrimaryKey() => GuidPrimaryKey.Parse(_formatted);

[Benchmark]
public bool TryParse_GuidPrimaryKey() => GuidPrimaryKey.TryParse(_formatted, out _);
}
46 changes: 46 additions & 0 deletions src/CompositeKey.Benchmarks/MixedCompositeKeyBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using BenchmarkDotNet.Attributes;

namespace CompositeKey.Benchmarks;

/// <summary>
/// Mixed-type composite key with partition/sort separation — exercises string.Create with exact length,
/// static lambda formatting, partition/sort splitting, and multi-type parsing.
/// </summary>
[CompositeKey("{GuidValue}#{DecimalValue}|Constant~{EnumValue}@{StringValue}", PrimaryKeySeparator = '|')]
public sealed partial record MixedCompositeKey(
Guid GuidValue,
decimal DecimalValue,
MixedCompositeKey.KeyEnumType EnumValue,
string StringValue)
{
public enum KeyEnumType { One, Two, Three }
}

[MemoryDiagnoser]
public class MixedCompositeKeyBenchmarks
{
private MixedCompositeKey _key = null!;
private string _formatted = null!;

[GlobalSetup]
public void Setup()
{
_key = new MixedCompositeKey(Guid.NewGuid(), 123.45m, MixedCompositeKey.KeyEnumType.Two, "test-value");
_formatted = _key.ToString();
}

[Benchmark]
public string ToString_MixedCompositeKey() => _key.ToString();

[Benchmark]
public string ToPartitionKeyString_MixedCompositeKey() => _key.ToPartitionKeyString();

[Benchmark]
public string ToSortKeyString_MixedCompositeKey() => _key.ToSortKeyString();

[Benchmark]
public MixedCompositeKey Parse_MixedCompositeKey() => MixedCompositeKey.Parse(_formatted);

[Benchmark]
public bool TryParse_MixedCompositeKey() => MixedCompositeKey.TryParse(_formatted, out _);
}
3 changes: 3 additions & 0 deletions src/CompositeKey.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using BenchmarkDotNet.Running;

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

Performance benchmarks for generated CompositeKey code using [BenchmarkDotNet](https://benchmarkdotnet.org/).

These benchmarks are **manual, developer-run** and are **not** part of the CI test suite.

## Running Benchmarks

Run all benchmarks:

```bash
dotnet run -c Release --project src/CompositeKey.Benchmarks -- --filter "*"
```

Run a specific benchmark class:

```bash
dotnet run -c Release --project src/CompositeKey.Benchmarks -- --filter "*GuidPrimaryKeyBenchmarks*"
```

Run a specific benchmark method:

```bash
dotnet run -c Release --project src/CompositeKey.Benchmarks -- --filter "*ToString_GuidPrimaryKey*"
```

Target a specific TFM for cross-runtime comparison:

```bash
dotnet run -c Release --project src/CompositeKey.Benchmarks --framework net10.0 -- --filter "*"
```

## Interpreting Results

BenchmarkDotNet produces a summary table with these key columns:

| Column | Description |
|----------------|------------------------------------------------------|
| **Mean** | Average execution time per operation |
| **Error** | Half of the 99.9% confidence interval |
| **StdDev** | Standard deviation of measurements |
| **Gen0** | Number of Gen 0 GC collections per 1000 operations |
| **Allocated** | Heap memory allocated per operation (bytes) |

Lower values are better for all columns. An `Allocated` value of `0 B` or `-` indicates a zero-allocation code path.

Results are written to `BenchmarkDotNet.Artifacts/` which is git-ignored.
33 changes: 33 additions & 0 deletions src/CompositeKey.Benchmarks/RepeatingCollectionKeyBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using BenchmarkDotNet.Attributes;

namespace CompositeKey.Benchmarks;

/// <summary>
/// Repeating collection key — exercises DefaultInterpolatedStringHandler formatting
/// and variable-length stackalloc parsing.
/// </summary>
[CompositeKey("{Type}#{Tags...,}")]
public sealed partial record RepeatingCollectionKey(string Type, List<string> Tags);

[MemoryDiagnoser]
public class RepeatingCollectionKeyBenchmarks
{
private RepeatingCollectionKey _key = null!;
private string _formatted = null!;

[GlobalSetup]
public void Setup()
{
_key = new RepeatingCollectionKey("entity", ["alpha", "beta", "gamma", "delta"]);
_formatted = _key.ToString();
}

[Benchmark]
public string ToString_RepeatingCollectionKey() => _key.ToString();

[Benchmark]
public RepeatingCollectionKey Parse_RepeatingCollectionKey() => RepeatingCollectionKey.Parse(_formatted);

[Benchmark]
public bool TryParse_RepeatingCollectionKey() => RepeatingCollectionKey.TryParse(_formatted, out _);
}
37 changes: 37 additions & 0 deletions src/CompositeKey.Benchmarks/RepeatingSpanParsableBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Immutable;
using BenchmarkDotNet.Attributes;

namespace CompositeKey.Benchmarks;

/// <summary>
/// Repeating SpanParsable key with same separator and ImmutableArray — exercises the same-separator
/// parse path (variable part count from initial split), ImmutableArray.CreateRange() construction,
/// and SpanParsable item parsing within the repeat loop.
/// </summary>
[CompositeKey("NODES#{Scores...#}")]
public sealed partial record RepeatingSpanParsableSameSeparatorKey(ImmutableArray<int> Scores);

[MemoryDiagnoser]
public class RepeatingSpanParsableBenchmarks
{
private RepeatingSpanParsableSameSeparatorKey _key = null!;
private string _formatted = null!;

[GlobalSetup]
public void Setup()
{
_key = new RepeatingSpanParsableSameSeparatorKey([100, 200, 300, 400]);
_formatted = _key.ToString();
}

[Benchmark]
public string ToString_RepeatingSpanParsable() => _key.ToString();

[Benchmark]
public RepeatingSpanParsableSameSeparatorKey Parse_RepeatingSpanParsable() =>
RepeatingSpanParsableSameSeparatorKey.Parse(_formatted);

[Benchmark]
public bool TryParse_RepeatingSpanParsable() =>
RepeatingSpanParsableSameSeparatorKey.TryParse(_formatted, out _);
}
Loading
Loading