Skip to content

Commit b090daf

Browse files
authored
Merge pull request #34 from lofcz/feat-tunit
move to TUnit, fix thread safety edge cases
2 parents 959a919 + eb09235 commit b090daf

56 files changed

Lines changed: 4910 additions & 3969 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<ProjectReference Include="..\src\FastCloner\FastCloner.csproj" />
10+
</ItemGroup>
11+
</Project>

.bench-harness-current/Program.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Diagnostics;
2+
using FastCloner;
3+
4+
var results = new List<(string Name, double Ms, double NsPerOp)>();
5+
Measure("SmallObject x100000", CreateSmallObject(), 2000, 100000, static x => FastCloner.FastCloner.DeepClone((SmallObject)x)!);
6+
Measure("StringArray1000 x20000", CreateStringArray(1000), 500, 20000, static x => FastCloner.FastCloner.DeepClone((string[])x)!);
7+
Measure("Dictionary50 x5000", CreateDictionary(50), 200, 5000, static x => FastCloner.FastCloner.DeepClone((Dictionary<string, SmallObject>)x)!);
8+
foreach (var (name, ms, nsPerOp) in results)
9+
Console.WriteLine($"{name}|{ms:F2}|{nsPerOp:F1}");
10+
11+
void Measure(string name, object value, int warmup, int iterations, Func<object, object> clone)
12+
{
13+
for (var i = 0; i < warmup; i++) _ = clone(value);
14+
GC.Collect();
15+
GC.WaitForPendingFinalizers();
16+
GC.Collect();
17+
var sw = Stopwatch.StartNew();
18+
for (var i = 0; i < iterations; i++) _ = clone(value);
19+
sw.Stop();
20+
results.Add((name, sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds * 1_000_000d / iterations));
21+
}
22+
23+
static SmallObject CreateSmallObject() => new() { Id = 123, Name = "small-object-name", CreatedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), IsActive = true, Score = 42.5 };
24+
static string[] CreateStringArray(int count) { var arr = new string[count]; for (var i = 0; i < count; i++) arr[i] = $"value-{i}"; return arr; }
25+
static Dictionary<string, SmallObject> CreateDictionary(int count) { var dict = new Dictionary<string, SmallObject>(count); for (var i = 0; i < count; i++) dict[$"key-{i}"] = new SmallObject { Id = i, Name = $"item-{i}", CreatedAt = new DateTime(2025, 1, 1).AddMinutes(i), IsActive = (i & 1) == 0, Score = i * 1.25 }; return dict; }
26+
27+
public sealed class SmallObject
28+
{
29+
public int Id { get; set; }
30+
public string Name { get; set; } = string.Empty;
31+
public DateTime CreatedAt { get; set; }
32+
public bool IsActive { get; set; }
33+
public double Score { get; set; }
34+
}

.github/workflows/benchmark.yml

Lines changed: 101 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ on:
2222

2323
env:
2424
DOTNET_VERSION: "10.0.103"
25+
BASELINE_BRANCH: "next"
2526

2627
permissions:
2728
contents: read
@@ -68,39 +69,45 @@ jobs:
6869

6970
- name: Restore benchmark project
7071
if: steps.run_gate.outputs.should_run == 'true'
71-
run: dotnet restore src/FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj
72+
working-directory: src
73+
run: dotnet restore FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj
7274

7375
- name: Run deep clone benchmarks
7476
if: steps.run_gate.outputs.should_run == 'true'
77+
working-directory: src
7578
shell: pwsh
7679
run: >
77-
dotnet run -c Release --project src/FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj -- --filter *DeepCloneBenchmarks*
80+
dotnet run -c Release --project FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj -- --filter *DeepCloneBenchmarks*
7881
7982
- name: Resolve benchmark CSV path
8083
if: steps.run_gate.outputs.should_run == 'true'
8184
shell: pwsh
8285
run: |
83-
$csv = Get-ChildItem -Path "BenchmarkDotNet.Artifacts/results" -Filter "*DeepCloneBenchmarks-report.csv" -Recurse |
86+
$csv = Get-ChildItem -Path "src/BenchmarkDotNet.Artifacts/results" -Filter "*DeepCloneBenchmarks-report.csv" -Recurse |
8487
Sort-Object LastWriteTime -Descending |
8588
Select-Object -First 1
8689
if (-not $csv) {
8790
throw "Could not find BenchmarkDotNet CSV output for DeepCloneBenchmarks."
8891
}
8992
9093
"BENCHMARK_CSV=$($csv.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append
91-
"RESULT_DIR=benchmark-results/${{ matrix.os }}" | Out-File -FilePath $env:GITHUB_ENV -Append
94+
"RESULT_DIR=$($env:GITHUB_WORKSPACE)/src/benchmark-results/${{ matrix.os }}" | Out-File -FilePath $env:GITHUB_ENV -Append
9295
93-
- name: Download latest baseline from next
96+
- name: Download latest baseline artifact
9497
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request'
98+
id: baseline_lookup
9599
shell: pwsh
96100
env:
97101
GH_TOKEN: ${{ github.token }}
98102
run: |
99103
$repo = "${{ github.repository }}"
100104
$workflowFile = "benchmark.yml"
101105
$artifactName = "deepclone-baseline-${{ matrix.os }}"
106+
$baselineBranch = "${{ env.BASELINE_BRANCH }}"
107+
108+
"baseline_branch=$baselineBranch" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
102109
103-
$runsResponse = gh api "repos/$repo/actions/workflows/$workflowFile/runs?branch=next&event=push&status=success&per_page=50"
110+
$runsResponse = gh api "repos/$repo/actions/workflows/$workflowFile/runs?branch=$baselineBranch&event=push&status=success&per_page=50"
104111
$runs = ($runsResponse | ConvertFrom-Json).workflow_runs
105112
$baselineRunId = $null
106113
@@ -121,7 +128,9 @@ jobs:
121128
}
122129
123130
if (-not $baselineRunId) {
124-
Write-Host "No baseline artifact found for '$artifactName'."
131+
Write-Host "No baseline artifact found for '$artifactName' on branch '$baselineBranch'. Falling back to slow-path baseline generation."
132+
"baseline_found=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
133+
"baseline_needs_slow_path=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
125134
exit 0
126135
}
127136
@@ -132,20 +141,68 @@ jobs:
132141
if ($baselineJson) {
133142
"BASELINE_JSON=$($baselineJson.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append
134143
Write-Host "Using baseline: $($baselineJson.FullName)"
144+
"baseline_found=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
145+
"baseline_needs_slow_path=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
135146
} else {
136-
Write-Host "Downloaded baseline artifact but current-normalized.json was not found."
147+
Write-Host "Downloaded baseline artifact but current-normalized.json was not found. Falling back to slow-path baseline generation."
148+
"baseline_found=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
149+
"baseline_needs_slow_path=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
150+
}
151+
152+
- name: Clone baseline branch for slow-path comparison
153+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request' && steps.baseline_lookup.outputs.baseline_needs_slow_path == 'true'
154+
shell: pwsh
155+
run: |
156+
$repo = "${{ github.repository }}"
157+
$baselineBranch = "${{ steps.baseline_lookup.outputs.baseline_branch }}"
158+
159+
if (Test-Path baseline-repo) {
160+
Remove-Item -Recurse -Force baseline-repo
161+
}
162+
163+
git clone --depth 1 --branch $baselineBranch "https://github.com/$repo.git" baseline-repo
164+
165+
- name: Restore slow-path baseline benchmark project
166+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request' && steps.baseline_lookup.outputs.baseline_needs_slow_path == 'true'
167+
working-directory: baseline-repo/src
168+
run: dotnet restore FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj
169+
170+
- name: Run slow-path baseline benchmarks
171+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request' && steps.baseline_lookup.outputs.baseline_needs_slow_path == 'true'
172+
working-directory: baseline-repo/src
173+
shell: pwsh
174+
run: >
175+
dotnet run -c Release --project FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj -- --filter *DeepCloneBenchmarks*
176+
177+
- name: Generate slow-path baseline normalized report
178+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request' && steps.baseline_lookup.outputs.baseline_needs_slow_path == 'true'
179+
working-directory: baseline-repo/src
180+
shell: pwsh
181+
run: |
182+
$csv = Get-ChildItem -Path "BenchmarkDotNet.Artifacts/results" -Filter "*DeepCloneBenchmarks-report.csv" -Recurse |
183+
Sort-Object LastWriteTime -Descending |
184+
Select-Object -First 1
185+
if (-not $csv) {
186+
throw "Could not find BenchmarkDotNet CSV output for slow-path baseline benchmarks."
137187
}
138188
189+
dotnet run -c Release --project FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj -- --report --csv $csv.FullName --normalized-json benchmark-results/current-normalized.json
190+
191+
$baselineJson = Resolve-Path "benchmark-results/current-normalized.json"
192+
"BASELINE_JSON=$($baselineJson.Path)" | Out-File -FilePath $env:GITHUB_ENV -Append
193+
Write-Host "Generated slow-path baseline: $($baselineJson.Path)"
194+
139195
- name: Generate normalized report and diff
140196
if: steps.run_gate.outputs.should_run == 'true'
197+
working-directory: src
141198
shell: pwsh
142199
run: |
143200
New-Item -ItemType Directory -Force -Path $env:RESULT_DIR | Out-Null
144201
145202
$args = @(
146203
"run",
147204
"-c", "Release",
148-
"--project", "src/FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj",
205+
"--project", "FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj",
149206
"--",
150207
"--report",
151208
"--csv", $env:BENCHMARK_CSV,
@@ -177,8 +234,8 @@ jobs:
177234
with:
178235
name: deepclone-results-${{ matrix.os }}
179236
path: |
180-
benchmark-results/${{ matrix.os }}/**
181-
BenchmarkDotNet.Artifacts/results/*DeepCloneBenchmarks*
237+
src/benchmark-results/${{ matrix.os }}/**
238+
src/BenchmarkDotNet.Artifacts/results/*DeepCloneBenchmarks*
182239
if-no-files-found: warn
183240
retention-days: 30
184241

@@ -187,7 +244,7 @@ jobs:
187244
uses: actions/upload-artifact@v4
188245
with:
189246
name: deepclone-baseline-${{ matrix.os }}
190-
path: benchmark-results/${{ matrix.os }}/current-normalized.json
247+
path: src/benchmark-results/${{ matrix.os }}/current-normalized.json
191248
if-no-files-found: error
192249
retention-days: 30
193250

@@ -198,15 +255,42 @@ jobs:
198255
GH_TOKEN: ${{ github.token }}
199256
run: |
200257
$commentPath = "$env:RESULT_DIR/pr-comment.md"
201-
if (-not (Test-Path $commentPath)) {
202-
throw "PR comment report not found: $commentPath"
203-
}
204-
205258
$marker = "<!-- deepclone-benchmark-report -->"
206-
$body = $marker + "`n" + (Get-Content $commentPath -Raw)
207259
$repo = "${{ github.repository }}"
208260
$prNumber = "${{ github.event.pull_request.number }}"
209261
262+
if (Test-Path $commentPath) {
263+
$body = $marker + "`n" + (Get-Content $commentPath -Raw)
264+
} else {
265+
$summaryPath = "$env:RESULT_DIR/summary.md"
266+
$currentReportPath = "$env:RESULT_DIR/current-report.md"
267+
268+
$fallback = @(
269+
'## Deep Clone Benchmarks'
270+
''
271+
'- OS: `${{ matrix.os }}`'
272+
'- Detailed PR benchmark report was not generated for this run.'
273+
)
274+
275+
if (Test-Path $summaryPath) {
276+
$fallback += ""
277+
$fallback += "### Summary"
278+
$fallback += ""
279+
$fallback += (Get-Content $summaryPath -Raw)
280+
} elseif (Test-Path $currentReportPath) {
281+
$fallback += ""
282+
$fallback += "### Current FastCloner vs DeepCloner"
283+
$fallback += ""
284+
$fallback += (Get-Content $currentReportPath -Raw)
285+
} else {
286+
$fallback += ""
287+
$fallback += "Benchmark artifacts should still be attached to this workflow run."
288+
}
289+
290+
$body = $marker + "`n" + ($fallback -join "`n")
291+
Write-Host "PR comment report not found at $commentPath. Posting fallback benchmark comment instead."
292+
}
293+
210294
$comments = gh api "repos/$repo/issues/$prNumber/comments?per_page=100" | ConvertFrom-Json
211295
$existing = $comments | Where-Object { $_.body -like "*$marker*" } | Select-Object -First 1
212296

.github/workflows/dotnet.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,16 @@ jobs:
3636
node-version: ${{ env.NODE_VERSION }}
3737

3838
- name: Restore dependencies
39-
run: dotnet restore src/FastClonerCi.slnf
39+
working-directory: src
40+
run: dotnet restore FastClonerCi.slnf
4041

4142
- name: Build
42-
run: dotnet build src/FastClonerCi.slnf --no-restore
43+
working-directory: src
44+
run: dotnet build FastClonerCi.slnf --no-restore
4345

4446
- name: Test
45-
run: dotnet test src/FastClonerCi.slnf --no-build --verbosity normal
47+
working-directory: src
48+
run: dotnet test --solution FastClonerCi.slnf --no-build --verbosity normal
4649

4750
- name: Update GitHub status check
4851
if: always()
@@ -73,7 +76,8 @@ jobs:
7376
node-version: ${{ env.NODE_VERSION }}
7477

7578
- name: Build FastCloner for netstandard2.0
76-
run: dotnet build src/FastCloner/ --framework netstandard2.0
79+
working-directory: src
80+
run: dotnet build FastCloner/ --framework netstandard2.0
7781

7882
- name: Update GitHub status check
7983
if: always()
@@ -104,7 +108,8 @@ jobs:
104108
node-version: ${{ env.NODE_VERSION }}
105109

106110
- name: Build FastCloner for net46
107-
run: dotnet build src/FastCloner/ --framework net46
111+
working-directory: src
112+
run: dotnet build FastCloner/ --framework net46
108113

109114
- name: Update GitHub status check
110115
if: always()

0 commit comments

Comments
 (0)