Skip to content

Commit ad5067f

Browse files
committed
feat(core): Comprehensive benchmark for codeclone has been added, the documentation has been updated, and the crash of tests in CI has been fixed
1 parent 20cbaad commit ad5067f

19 files changed

Lines changed: 1022 additions & 52 deletions

.dockerignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.git
2+
.cache
3+
.venv
4+
.pytest_cache
5+
.mypy_cache
6+
.ruff_cache
7+
.idea
8+
__pycache__/
9+
*.pyc
10+
*.pyo
11+
*.pyd
12+
.coverage
13+
build/
14+
dist/
15+
*.egg-info/
16+
.uv-cache
17+
docs
18+
codeclone.egg-info
19+
.pre-commit-config.yaml
20+
uv.lock

.github/workflows/benchmark.yml

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
name: benchmark
2+
run-name: benchmark • ${{ github.event_name }} • ${{ github.ref_name }}
3+
4+
on:
5+
push:
6+
branches: [ "feat/2.0.0" ]
7+
pull_request:
8+
branches: [ "feat/2.0.0" ]
9+
workflow_dispatch:
10+
inputs:
11+
profile:
12+
description: Benchmark profile
13+
required: true
14+
default: smoke
15+
type: choice
16+
options:
17+
- smoke
18+
- extended
19+
20+
permissions:
21+
contents: read
22+
23+
concurrency:
24+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
25+
cancel-in-progress: true
26+
27+
jobs:
28+
benchmark:
29+
name: >-
30+
bench • ${{ matrix.label }}
31+
runs-on: ${{ matrix.os }}
32+
timeout-minutes: ${{ matrix.timeout_minutes }}
33+
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
include:
38+
# default profile for push / PR
39+
- profile: smoke
40+
label: linux-smoke
41+
os: ubuntu-latest
42+
runs: 12
43+
warmups: 3
44+
cpus: "1.0"
45+
memory: "2g"
46+
timeout_minutes: 45
47+
48+
# extended profile for manual runs
49+
- profile: extended
50+
label: linux-extended
51+
os: ubuntu-latest
52+
runs: 16
53+
warmups: 4
54+
cpus: "1.0"
55+
memory: "2g"
56+
timeout_minutes: 50
57+
58+
- profile: extended
59+
label: macos-extended
60+
os: macos-latest
61+
runs: 12
62+
warmups: 3
63+
cpus: ""
64+
memory: ""
65+
timeout_minutes: 60
66+
67+
if: >
68+
(github.event_name != 'workflow_dispatch' && matrix.profile == 'smoke') ||
69+
(github.event_name == 'workflow_dispatch' && matrix.profile == inputs.profile)
70+
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@v6
74+
75+
- name: Set benchmark output path
76+
shell: bash
77+
run: |
78+
mkdir -p .cache/benchmarks
79+
echo "BENCH_JSON=.cache/benchmarks/codeclone-benchmark-${{ matrix.label }}.json" >> "$GITHUB_ENV"
80+
81+
- name: Build and run Docker benchmark (Linux)
82+
if: runner.os == 'Linux'
83+
env:
84+
RUNS: ${{ matrix.runs }}
85+
WARMUPS: ${{ matrix.warmups }}
86+
CPUS: ${{ matrix.cpus }}
87+
MEMORY: ${{ matrix.memory }}
88+
run: |
89+
./benchmarks/run_docker_benchmark.sh
90+
cp .cache/benchmarks/codeclone-benchmark.json "$BENCH_JSON"
91+
92+
- name: Run local benchmark (macOS)
93+
if: runner.os == 'macOS'
94+
run: |
95+
uv run python benchmarks/run_benchmark.py \
96+
--target . \
97+
--runs "${{ matrix.runs }}" \
98+
--warmups "${{ matrix.warmups }}" \
99+
--tmp-dir "/tmp/codeclone-bench-${{ matrix.label }}" \
100+
--output "$BENCH_JSON"
101+
102+
- name: Print benchmark summary
103+
if: always()
104+
shell: bash
105+
run: |
106+
python - <<'PY'
107+
import json
108+
import os
109+
from pathlib import Path
110+
111+
report_path = Path(os.environ["BENCH_JSON"])
112+
if not report_path.exists():
113+
print(f"benchmark report not found: {report_path}")
114+
raise SystemExit(1)
115+
116+
payload = json.loads(report_path.read_text(encoding="utf-8"))
117+
scenarios = payload.get("scenarios", [])
118+
comparisons = payload.get("comparisons", {})
119+
120+
print("CodeClone benchmark summary")
121+
print(f"label={os.environ.get('RUNNER_OS','unknown').lower()} / {os.environ.get('GITHUB_JOB','benchmark')}")
122+
for scenario in scenarios:
123+
name = str(scenario.get("name", "unknown"))
124+
stats = scenario.get("stats_seconds", {})
125+
median = float(stats.get("median", 0.0))
126+
p95 = float(stats.get("p95", 0.0))
127+
stdev = float(stats.get("stdev", 0.0))
128+
digest = str(scenario.get("digest", ""))
129+
print(
130+
f"- {name:16s} median={median:.4f}s "
131+
f"p95={p95:.4f}s stdev={stdev:.4f}s digest={digest}"
132+
)
133+
134+
if comparisons:
135+
print("ratios:")
136+
for key, value in sorted(comparisons.items()):
137+
print(f"- {key}={float(value):.3f}x")
138+
139+
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
140+
if not summary_file:
141+
raise SystemExit(0)
142+
143+
lines = [
144+
f"## CodeClone benchmark — {os.environ.get('RUNNER_OS', 'unknown')} / ${{ matrix.label }}",
145+
"",
146+
f"- Tool: `{payload['tool']['name']} {payload['tool']['version']}`",
147+
f"- Target: `{payload['config']['target']}`",
148+
f"- Runs: `{payload['config']['runs']}`",
149+
f"- Warmups: `{payload['config']['warmups']}`",
150+
f"- Generated: `{payload['generated_at_utc']}`",
151+
"",
152+
"### Scenarios",
153+
"",
154+
"| Scenario | Median (s) | p95 (s) | Stdev (s) | Deterministic | Digest |",
155+
"|---|---:|---:|---:|:---:|---|",
156+
]
157+
158+
for scenario in scenarios:
159+
stats = scenario.get("stats_seconds", {})
160+
lines.append(
161+
"| "
162+
f"{scenario.get('name', '')} | "
163+
f"{float(stats.get('median', 0.0)):.4f} | "
164+
f"{float(stats.get('p95', 0.0)):.4f} | "
165+
f"{float(stats.get('stdev', 0.0)):.4f} | "
166+
f"{'yes' if bool(scenario.get('deterministic')) else 'no'} | "
167+
f"{scenario.get('digest', '')} |"
168+
)
169+
170+
if comparisons:
171+
lines.extend(
172+
[
173+
"",
174+
"### Ratios",
175+
"",
176+
"| Metric | Value |",
177+
"|---|---:|",
178+
]
179+
)
180+
for key, value in sorted(comparisons.items()):
181+
lines.append(f"| {key} | {float(value):.3f}x |")
182+
183+
with Path(summary_file).open("a", encoding="utf-8") as fh:
184+
fh.write("\n".join(lines) + "\n")
185+
PY
186+
187+
- name: Upload benchmark artifact
188+
if: always()
189+
uses: actions/upload-artifact@v7
190+
with:
191+
name: codeclone-benchmark-${{ matrix.label }}
192+
path: ${{ env.BENCH_JSON }}
193+
if-no-files-found: error

README.md

Lines changed: 106 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ codeclone . --json --md --sarif --text # generate machine-readable reports
4242
codeclone . --ci # CI mode (--fail-on-new --no-color --quiet)
4343
```
4444

45+
## Reproducible Docker Benchmark
46+
47+
```bash
48+
./benchmarks/run_docker_benchmark.sh
49+
```
50+
51+
The wrapper builds `benchmarks/Dockerfile`, runs isolated container benchmarks, and
52+
writes deterministic results to `.cache/benchmarks/codeclone-benchmark.json`.
53+
Use environment overrides to pin benchmark envelope:
54+
55+
```bash
56+
CPUSET=0 CPUS=1.0 MEMORY=2g RUNS=16 WARMUPS=4 \
57+
./benchmarks/run_docker_benchmark.sh
58+
```
59+
4560
<details>
4661
<summary>Run without install</summary>
4762

@@ -62,6 +77,8 @@ codeclone . --ci
6277
```
6378

6479
The `--ci` preset equals `--fail-on-new --no-color --quiet`.
80+
When a trusted metrics baseline is loaded, CI mode also enables
81+
`--fail-on-new-metrics`.
6582

6683
### Quality Gates
6784

@@ -135,13 +152,13 @@ Contract errors (`2`) take precedence over gating failures (`3`).
135152

136153
## Reports
137154

138-
| Format | Flag | Default path |
139-
|--------|----------|--------------------------------|
140-
| HTML | `--html` | `.cache/codeclone/report.html` |
141-
| JSON | `--json` | `.cache/codeclone/report.json` |
142-
| Markdown | `--md` | `.cache/codeclone/report.md` |
143-
| SARIF | `--sarif` | `.cache/codeclone/report.sarif` |
144-
| Text | `--text` | `.cache/codeclone/report.txt` |
155+
| Format | Flag | Default path |
156+
|----------|-----------|---------------------------------|
157+
| HTML | `--html` | `.cache/codeclone/report.html` |
158+
| JSON | `--json` | `.cache/codeclone/report.json` |
159+
| Markdown | `--md` | `.cache/codeclone/report.md` |
160+
| SARIF | `--sarif` | `.cache/codeclone/report.sarif` |
161+
| Text | `--text` | `.cache/codeclone/report.txt` |
145162

146163
All report formats are rendered from one canonical JSON report document.
147164

@@ -154,32 +171,73 @@ All report formats are rendered from one canonical JSON report document.
154171
"meta": {
155172
"codeclone_version": "2.0.0b1",
156173
"project_name": "...",
157-
"scan_root": "...",
174+
"scan_root": ".",
158175
"report_mode": "full",
159-
"baseline": { "...": "..." },
160-
"cache": { "...": "..." },
161-
"metrics_baseline": { "...": "..." },
162-
"runtime": { "report_generated_at_utc": "..." }
176+
"baseline": {
177+
"...": "..."
178+
},
179+
"cache": {
180+
"...": "..."
181+
},
182+
"metrics_baseline": {
183+
"...": "..."
184+
},
185+
"runtime": {
186+
"report_generated_at_utc": "..."
187+
}
163188
},
164189
"inventory": {
165-
"files": { "...": "..." },
166-
"code": { "...": "..." },
167-
"file_registry": { "encoding": "relative_path", "items": [] }
190+
"files": {
191+
"...": "..."
192+
},
193+
"code": {
194+
"...": "..."
195+
},
196+
"file_registry": {
197+
"encoding": "relative_path",
198+
"items": []
199+
}
168200
},
169201
"findings": {
170-
"summary": { "...": "..." },
202+
"summary": {
203+
"...": "..."
204+
},
171205
"groups": {
172-
"clones": { "functions": [], "blocks": [], "segments": [] },
173-
"structural": { "groups": [] },
174-
"dead_code": { "groups": [] },
175-
"design": { "groups": [] }
206+
"clones": {
207+
"functions": [],
208+
"blocks": [],
209+
"segments": []
210+
},
211+
"structural": {
212+
"groups": []
213+
},
214+
"dead_code": {
215+
"groups": []
216+
},
217+
"design": {
218+
"groups": []
219+
}
176220
}
177221
},
178-
"metrics": { "summary": {}, "families": {} },
179-
"derived": { "suggestions": [], "overview": {}, "hotlists": {} },
222+
"metrics": {
223+
"summary": {},
224+
"families": {}
225+
},
226+
"derived": {
227+
"suggestions": [],
228+
"overview": {},
229+
"hotlists": {}
230+
},
180231
"integrity": {
181-
"canonicalization": { "version": "1", "scope": "canonical_only" },
182-
"digest": { "algorithm": "sha256", "verified": true, "value": "..." }
232+
"canonicalization": {
233+
"version": "1",
234+
"scope": "canonical_only"
235+
},
236+
"digest": {
237+
"algorithm": "sha256",
238+
"verified": true,
239+
"value": "..."
240+
}
183241
}
184242
}
185243
```
@@ -212,8 +270,32 @@ Architecture: [`docs/architecture.md`](docs/architecture.md) · CFG semantics: [
212270
| Report contract | [`docs/book/08-report.md`](docs/book/08-report.md) |
213271
| Metrics & quality gates | [`docs/book/15-metrics-and-quality-gates.md`](docs/book/15-metrics-and-quality-gates.md) |
214272
| Dead code | [`docs/book/16-dead-code-contract.md`](docs/book/16-dead-code-contract.md) |
273+
| Docker benchmark contract | [`docs/book/18-benchmarking.md`](docs/book/18-benchmarking.md) |
215274
| Determinism | [`docs/book/12-determinism.md`](docs/book/12-determinism.md) |
216275

276+
<details>
277+
<summary>Benchmarking</summary>
278+
279+
## Reproducible Docker Benchmark
280+
281+
```bash
282+
./benchmarks/run_docker_benchmark.sh
283+
```
284+
285+
The wrapper builds `benchmarks/Dockerfile`, runs isolated container benchmarks, and writes results to
286+
`.cache/benchmarks/codeclone-benchmark.json`.
287+
288+
Use environment overrides to pin the benchmark envelope:
289+
290+
```bash
291+
CPUSET=0 CPUS=1.0 MEMORY=2g RUNS=16 WARMUPS=4 \
292+
./benchmarks/run_docker_benchmark.sh
293+
```
294+
295+
Benchmark contract: [docs/book/18-benchmarking.md](docs/book/18-benchmarking.md)
296+
297+
</details>
298+
217299
## Links
218300

219301
- **Issues:** <https://github.com/orenlab/codeclone/issues>

0 commit comments

Comments
 (0)