Skip to content

Commit b75c435

Browse files
committed
run benchmarks on ci
1 parent f86b21c commit b75c435

File tree

12 files changed

+1851
-7
lines changed

12 files changed

+1851
-7
lines changed

.github/workflows/benchmark.yml

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
name: Deep Clone Benchmarks
2+
3+
on:
4+
push:
5+
branches: [ "next" ]
6+
pull_request:
7+
branches: [ "next" ]
8+
workflow_dispatch:
9+
inputs:
10+
run_ubuntu:
11+
description: "Run Ubuntu benchmarks"
12+
type: boolean
13+
default: true
14+
run_windows:
15+
description: "Run Windows benchmarks"
16+
type: boolean
17+
default: true
18+
run_macos:
19+
description: "Run macOS benchmarks"
20+
type: boolean
21+
default: true
22+
23+
env:
24+
DOTNET_VERSION: "10.0.103"
25+
26+
permissions:
27+
contents: read
28+
actions: read
29+
pull-requests: write
30+
31+
jobs:
32+
benchmark:
33+
name: Benchmarks (${{ matrix.os }})
34+
runs-on: ${{ matrix.os }}
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
os: [ubuntu-latest, windows-latest, macos-latest]
39+
40+
steps:
41+
- name: Select target OS execution
42+
id: run_gate
43+
shell: pwsh
44+
run: |
45+
$eventName = "${{ github.event_name }}"
46+
$os = "${{ matrix.os }}"
47+
$shouldRun = $false
48+
49+
if ($eventName -ne "workflow_dispatch") {
50+
$shouldRun = $os -eq "ubuntu-latest"
51+
} else {
52+
switch ($os) {
53+
"ubuntu-latest" { $shouldRun = "${{ inputs.run_ubuntu }}" -eq "true"; break }
54+
"windows-latest" { $shouldRun = "${{ inputs.run_windows }}" -eq "true"; break }
55+
"macos-latest" { $shouldRun = "${{ inputs.run_macos }}" -eq "true"; break }
56+
}
57+
}
58+
59+
"should_run=$($shouldRun.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
60+
61+
- name: Checkout repository
62+
if: steps.run_gate.outputs.should_run == 'true'
63+
uses: actions/checkout@v4
64+
65+
- name: Setup .NET
66+
if: steps.run_gate.outputs.should_run == 'true'
67+
uses: actions/setup-dotnet@v4
68+
with:
69+
dotnet-version: ${{ env.DOTNET_VERSION }}
70+
71+
- name: Restore benchmark project
72+
if: steps.run_gate.outputs.should_run == 'true'
73+
run: dotnet restore src/FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj
74+
75+
- name: Run deep clone benchmarks
76+
if: steps.run_gate.outputs.should_run == 'true'
77+
shell: pwsh
78+
run: >
79+
dotnet run -c Release --project src/FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj -- --filter *DeepCloneBenchmarks*
80+
81+
- name: Resolve benchmark CSV path
82+
if: steps.run_gate.outputs.should_run == 'true'
83+
shell: pwsh
84+
run: |
85+
$csv = Get-ChildItem -Path "BenchmarkDotNet.Artifacts/results" -Filter "*DeepCloneBenchmarks-report.csv" -Recurse |
86+
Sort-Object LastWriteTime -Descending |
87+
Select-Object -First 1
88+
if (-not $csv) {
89+
throw "Could not find BenchmarkDotNet CSV output for DeepCloneBenchmarks."
90+
}
91+
92+
"BENCHMARK_CSV=$($csv.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append
93+
"RESULT_DIR=benchmark-results/${{ matrix.os }}" | Out-File -FilePath $env:GITHUB_ENV -Append
94+
95+
- name: Download latest baseline from next
96+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request'
97+
shell: pwsh
98+
env:
99+
GH_TOKEN: ${{ github.token }}
100+
run: |
101+
$repo = "${{ github.repository }}"
102+
$workflowFile = "benchmark.yml"
103+
$artifactName = "deepclone-baseline-${{ matrix.os }}"
104+
105+
$runsResponse = gh api "repos/$repo/actions/workflows/$workflowFile/runs?branch=next&event=push&status=success&per_page=50"
106+
$runs = ($runsResponse | ConvertFrom-Json).workflow_runs
107+
$artifactId = $null
108+
109+
foreach ($run in $runs) {
110+
if ($run.id -eq ${{ github.run_id }}) {
111+
continue
112+
}
113+
114+
$artifactsResponse = gh api "repos/$repo/actions/runs/$($run.id)/artifacts?per_page=100"
115+
$artifact = ($artifactsResponse | ConvertFrom-Json).artifacts |
116+
Where-Object { $_.name -eq $artifactName -and -not $_.expired } |
117+
Select-Object -First 1
118+
119+
if ($artifact) {
120+
$artifactId = $artifact.id
121+
break
122+
}
123+
}
124+
125+
if (-not $artifactId) {
126+
Write-Host "No baseline artifact found for '$artifactName'."
127+
exit 0
128+
}
129+
130+
New-Item -ItemType Directory -Force -Path baseline | Out-Null
131+
gh api "repos/$repo/actions/artifacts/$artifactId/zip" --output baseline/baseline.zip
132+
Expand-Archive -Path baseline/baseline.zip -DestinationPath baseline -Force
133+
Remove-Item baseline/baseline.zip -Force
134+
135+
$baselineJson = Get-ChildItem -Path baseline -Filter "current-normalized.json" -Recurse | Select-Object -First 1
136+
if ($baselineJson) {
137+
"BASELINE_JSON=$($baselineJson.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append
138+
Write-Host "Using baseline: $($baselineJson.FullName)"
139+
} else {
140+
Write-Host "Downloaded baseline artifact but current-normalized.json was not found."
141+
}
142+
143+
- name: Generate normalized report and diff
144+
if: steps.run_gate.outputs.should_run == 'true'
145+
shell: pwsh
146+
run: |
147+
New-Item -ItemType Directory -Force -Path $env:RESULT_DIR | Out-Null
148+
149+
$args = @(
150+
"run",
151+
"-c", "Release",
152+
"--project", "src/FastCloner.Benchmark.CI/FastCloner.Benchmark.CI.csproj",
153+
"--",
154+
"--report",
155+
"--csv", $env:BENCHMARK_CSV,
156+
"--normalized-json", "$env:RESULT_DIR/current-normalized.json",
157+
"--current-report", "$env:RESULT_DIR/current-report.md",
158+
"--summary-report", "$env:RESULT_DIR/summary.md",
159+
"--comment-report", "$env:RESULT_DIR/pr-comment.md",
160+
"--diff-json", "$env:RESULT_DIR/baseline-diff.json",
161+
"--os", "${{ matrix.os }}"
162+
)
163+
164+
if ($env:BASELINE_JSON) {
165+
$args += @("--baseline-json", $env:BASELINE_JSON)
166+
}
167+
168+
dotnet @args
169+
170+
- name: Add benchmark summary to workflow
171+
if: always() && steps.run_gate.outputs.should_run == 'true'
172+
shell: pwsh
173+
run: |
174+
if (Test-Path "$env:RESULT_DIR/summary.md") {
175+
Get-Content "$env:RESULT_DIR/summary.md" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append
176+
}
177+
178+
- name: Upload benchmark result artifacts
179+
if: always() && steps.run_gate.outputs.should_run == 'true'
180+
uses: actions/upload-artifact@v4
181+
with:
182+
name: deepclone-results-${{ matrix.os }}
183+
path: |
184+
benchmark-results/${{ matrix.os }}/**
185+
BenchmarkDotNet.Artifacts/results/*DeepCloneBenchmarks*
186+
if-no-files-found: warn
187+
retention-days: 30
188+
189+
- name: Publish baseline artifact
190+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/next'
191+
uses: actions/upload-artifact@v4
192+
with:
193+
name: deepclone-baseline-${{ matrix.os }}
194+
path: benchmark-results/${{ matrix.os }}/current-normalized.json
195+
if-no-files-found: error
196+
retention-days: 30
197+
198+
- name: Upsert pull request benchmark comment
199+
if: steps.run_gate.outputs.should_run == 'true' && github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest'
200+
shell: pwsh
201+
env:
202+
GH_TOKEN: ${{ github.token }}
203+
run: |
204+
$commentPath = "$env:RESULT_DIR/pr-comment.md"
205+
if (-not (Test-Path $commentPath)) {
206+
throw "PR comment report not found: $commentPath"
207+
}
208+
209+
$marker = "<!-- deepclone-benchmark-report -->"
210+
$body = $marker + "`n" + (Get-Content $commentPath -Raw)
211+
$repo = "${{ github.repository }}"
212+
$prNumber = "${{ github.event.pull_request.number }}"
213+
214+
$comments = gh api "repos/$repo/issues/$prNumber/comments?per_page=100" | ConvertFrom-Json
215+
$existing = $comments | Where-Object { $_.body -like "*$marker*" } | Select-Object -First 1
216+
217+
$payloadPath = Join-Path $env:RUNNER_TEMP "deepclone-comment.json"
218+
@{ body = $body } | ConvertTo-Json -Depth 10 | Set-Content -Path $payloadPath
219+
220+
if ($existing) {
221+
gh api --method PATCH "repos/$repo/issues/comments/$($existing.id)" --input $payloadPath | Out-Null
222+
Write-Host "Updated existing benchmark PR comment."
223+
} else {
224+
gh api --method POST "repos/$repo/issues/$prNumber/comments" --input $payloadPath | Out-Null
225+
Write-Host "Created benchmark PR comment."
226+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ Intel Core i7-8700 CPU 3.20GHz (Max: 3.19GHz) (Coffee Lake), 1 CPU, 12 logical a
352352
| AnyCloneBenchmark | 5,102.40 ns | 239.089 ns | 704.959 ns | 5,370.93 ns | 497.81 | 68.98 | 13 | 0.9003 | - | 5656 B | 78.56 |
353353
```
354354

355-
You can run the benchmark [locally](https://github.com/lofcz/FastCloner/blob/next/src/FastCloner.Benchmark/BenchMinimal.cs) to verify the results. There are also [third-party benchmarks](https://github.com/AnderssonPeter/Dolly?tab=readme-ov-file#benchmarks) in some of the competing libraries confirming these results.
355+
You can run the benchmark [locally](https://github.com/lofcz/FastCloner/blob/next/src/FastCloner.Benchmark/BenchMinimal.cs) to verify the results. For CI regression tracking focused on DeepCloner vs FastCloner, see [FastCloner.Benchmark.CI](src/FastCloner.Benchmark.CI/README.md). There are also [third-party benchmarks](https://github.com/AnderssonPeter/Dolly?tab=readme-ov-file#benchmarks) in some of the competing libraries confirming these results.
356356

357357
### Build Times & IDE Performance
358358

0 commit comments

Comments
 (0)