Skip to content
Next Next commit
feat: add benchmark
  • Loading branch information
shenxianpeng committed Aug 31, 2025
commit 89d7faa357543da53d4a028ce7882871742466fc
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ venv
result.txt
testing/main.c
*/*compile_commands.json
testing/benchmark_results.txt

# Ignore Python wheel packages (clang-format, clang-tidy)
clang-tidy-1*
Expand Down
18 changes: 18 additions & 0 deletions docs/benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Benchmarking

This document outlines the benchmarking process for comparing the performance of cpp-linter-hooks and mirrors-clang-format.

## Running the Benchmark

```bash
python3 testing/benchmark_hooks.py
```

## Results

The results of the benchmarking process will be saved to `testing/benchmark_results.txt`.
Comment on lines +13 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Call out cache-warming to ensure fair, stable timings.

Make it explicit that cold-cache runs will dominate timings unless caches are warmed.

 ## Results -The results of the benchmarking process will be saved to `testing/benchmark_results.txt`. +The results of the benchmarking process will be saved to `testing/benchmark_results.txt`. + +Note: For fair comparisons, warm pre-commit caches once per hook (do not clean between repeats). If you need cold-cache numbers, run a separate pass that cleans caches before the first run only.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Results
The results of the benchmarking process will be saved to `testing/benchmark_results.txt`.
## Results
The results of the benchmarking process will be saved to `testing/benchmark_results.txt`.
Note: For fair comparisons, warm pre-commit caches once per hook (do not clean between repeats). If you need cold-cache numbers, run a separate pass that cleans caches before the first run only.
🤖 Prompt for AI Agents
In docs/benchmark.md around lines 11 to 13, the Results section does not mention cache-warming and thus can mislead readers because cold-cache runs will dominate timings; update the text to call out cache-warming explicitly and prescribe a procedure: run a number of warm-up iterations before timed runs (or document how to clear caches for cold runs), run each benchmark multiple times, report both cold and warm timings or the averaged warm timings, and note any cache/state-reset steps used so results are fair and reproducible. 

## To Do

- Run benchmark against a larger codebase, such as [TheAlgorithms/C-Plus-Plus](https://github.com/TheAlgorithms/C-Plus-Plus).
- Run benchmark with GitHub Actions for continuous integration.
95 changes: 95 additions & 0 deletions testing/benchmark_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Benchmark script to compare performance of cpp-linter-hooks vs mirrors-clang-format.

Usage:
python benchmark_hooks.py

Requirements:
- pre-commit must be installed and available in PATH
- Two config files:
- testing/pre-commit-config-cpp-linter-hooks.yaml
- testing/pre-commit-config-mirrors-clang-format.yaml
- Target files: testing/main.c (or adjust as needed)
"""
Comment on lines +5 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix docstring: wrong paths and usage.

The guidance doesn’t match the actual file path and config filenames.

-Usage: - python benchmark_hooks.py +Usage: + python testing/benchmark_hooks.py @@ -- Two config files: - - testing/pre-commit-config-cpp-linter-hooks.yaml - - testing/pre-commit-config-mirrors-clang-format.yaml -- Target files: testing/main.c (or adjust as needed) +- Two config files: + - testing/benchmark_hook_1.yaml + - testing/benchmark_hook_2.yaml +- Target files are auto-discovered under testing/test-examples/ after cloning.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In testing/benchmark_hooks.py around lines 5 to 14, the module docstring contains incorrect usage text and wrong config file paths/names; update the docstring to show the actual invocation to run this script (reflect whether it should be run as python testing/benchmark_hooks.py or python -m testing.benchmark_hooks), replace the two listed config filenames and their paths with the real config files present in the repo, and correct the example target file path (or note it is adjustable) so the docstring accurately matches the repository layout and how to run the script. 

import subprocess
import time
import statistics
import glob

HOOKS = [
{
"name": "cpp-linter-hooks",
"config": "testing/pre-commit-config-cpp-linter-hooks.yaml",
},
{
"name": "mirrors-clang-format",
"config": "testing/pre-commit-config-mirrors-clang-format.yaml",
},
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify configs exist before running.

A missing testing/pre-commit-config-*.yaml will fail late. Add a preflight check and exit with a clear message.

Proposed check (add near HOOKS or in main):

for cfg in (h["config"] for h in HOOKS): if not Path(cfg).is_file(): raise SystemExit(f"Missing pre-commit config: {cfg}")

To verify in your branch:


🏁 Script executed:

#!/bin/bash set -e test -f testing/pre-commit-config-cpp-linter-hooks.yaml && echo "OK: cpp-linter-hooks config present" test -f testing/pre-commit-config-mirrors-clang-format.yaml && echo "OK: mirrors-clang-format config present"

Length of output: 128


🏁 Script executed:

#!/bin/bash ls -la testing

Length of output: 910


Add preflight check and fix config paths

  • HOOKS lists testing/pre-commit-config-cpp-linter-hooks.yaml and testing/pre-commit-config-mirrors-clang-format.yaml, but neither file exists under testing/. Add the missing files or update HOOKS to reference the correct pre-commit-config*.yaml filenames.
  • Before loading, validate each config path and exit with a clear error if it’s missing, e.g.:
    for cfg in (h["config"] for h in HOOKS): if not Path(cfg).is_file(): raise SystemExit(f"Missing pre-commit config: {cfg}")
🤖 Prompt for AI Agents
testing/benchmark_hooks.py lines 21-30: HOOKS contains config paths that don't exist under testing/ and there's no preflight validation; either correct the filenames in HOOKS to the actual pre-commit-config*.yaml locations (or add the missing files into testing/), and add a preflight check before using HOOKS that iterates over each h["config"], uses pathlib.Path(cfg).is_file() to verify existence, and raises SystemExit(f"Missing pre-commit config: {cfg}") (ensure pathlib.Path is imported if not already). 

# Automatically find all C/C++ files in testing/ (and optionally src/, include/)
TARGET_FILES = (
glob.glob("testing/**/*.c", recursive=True)
+ glob.glob("testing/**/*.cpp", recursive=True)
+ glob.glob("testing/**/*.h", recursive=True)
+ glob.glob("testing/**/*.hpp", recursive=True)
)

REPEATS = 5
RESULTS_FILE = "testing/benchmark_results.txt"


def run_hook(config, files):
cmd = ["pre-commit", "run", "--config", config, "--files"] + files
start = time.perf_counter()
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError:
# Still record time even if hook fails
pass
end = time.perf_counter()
return end - start

Comment on lines +57 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden subprocess call: use iterable unpacking, capture_output, and a timeout.

Prevents hangs from blocking hooks and addresses style nits.

-def run_hook(config, files): - cmd = ["pre-commit", "run", "--config", config, "--files"] + files +def run_hook(config, files): + cmd = ["pre-commit", "run", "--config", config, "--files", *files] start = time.perf_counter() try: - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run(cmd, check=True, capture_output=True, timeout=300) except subprocess.CalledProcessError: # Still record time even if hook fails pass + except subprocess.TimeoutExpired: + # Record as a timeout-run; caller still gets elapsed wall time + pass end = time.perf_counter() return end - start
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def run_hook(config, files):
cmd = ["pre-commit", "run", "--config", config, "--files"] + files
start = time.perf_counter()
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError:
# Still record time even if hook fails
pass
end = time.perf_counter()
return end - start
def run_hook(config, files):
cmd = ["pre-commit", "run", "--config", config, "--files", *files]
start = time.perf_counter()
try:
subprocess.run(cmd, check=True, capture_output=True, timeout=300)
except subprocess.CalledProcessError:
# Still record time even if hook fails
pass
except subprocess.TimeoutExpired:
# Record as a timeout-run; caller still gets elapsed wall time
pass
end = time.perf_counter()
return end - start
🧰 Tools
🪛 Ruff (0.12.2)

45-45: Consider iterable unpacking instead of concatenation

Replace with iterable unpacking

(RUF005)


48-48: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
In testing/benchmark_hooks.py around lines 44-54, the subprocess.run call should be hardened: construct the command using iterable unpacking for files (e.g., ["pre-commit","run","--config", config,"--files", *files]), call subprocess.run with capture_output=True instead of stdout/stderr, and pass a sensible timeout (e.g., timeout=300) to avoid hangs; catch both subprocess.CalledProcessError and subprocess.TimeoutExpired (still recording elapsed time and returning it) so failing or timed-out hooks don't block the benchmark. 

def benchmark():
results = {}
for hook in HOOKS:
times = []
print(f"Benchmarking {hook['name']}...")
for i in range(REPEATS):
# Clean up any changes before each run
subprocess.run(["git", "restore"] + TARGET_FILES)
subprocess.run(["pre-commit", "clean"])
t = run_hook(hook["config"], TARGET_FILES)
print(f" Run {i + 1}: {t:.3f} seconds")
times.append(t)
results[hook["name"]] = times
return results

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t clean caches on every repeat; warm once per hook and restore files correctly.

Cleaning inside the loop measures install time/network, not hook performance. Also restore both worktree and index to HEAD for deterministic state.

 def benchmark(): results = {} for hook in HOOKS: times = [] print(f"Benchmarking {hook['name']}...") - for i in range(REPEATS): - # Clean up any changes before each run - subprocess.run(["git", "restore"] + TARGET_FILES) - subprocess.run(["pre-commit", "clean"]) - t = run_hook(hook["config"], TARGET_FILES) + # Ensure no cross-hook cache effects, but keep caches across repeats + subprocess.run(["pre-commit", "clean"], check=False) + # Warm caches once (untimed) + print(" Warming caches (not timed)...") + _ = run_hook(hook["config"], TARGET_FILES) + for i in range(REPEATS): + # Reset files to a known clean state before each timed run + subprocess.run( + ["git", "restore", "--staged", "--worktree", "--source", "HEAD", "--", *TARGET_FILES], + check=False, + ) + t = run_hook(hook["config"], TARGET_FILES) print(f" Run {i + 1}: {t:.3f} seconds") times.append(t) results[hook["name"]] = times return results
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def benchmark():
results = {}
for hook in HOOKS:
times = []
print(f"Benchmarking {hook['name']}...")
for i in range(REPEATS):
# Clean up any changes before each run
subprocess.run(["git", "restore"] + TARGET_FILES)
subprocess.run(["pre-commit", "clean"])
t = run_hook(hook["config"], TARGET_FILES)
print(f" Run {i + 1}: {t:.3f} seconds")
times.append(t)
results[hook["name"]] = times
return results
def benchmark():
results = {}
for hook in HOOKS:
times = []
print(f"Benchmarking {hook['name']}...")
# Ensure no cross-hook cache effects, but keep caches across repeats
subprocess.run(["pre-commit", "clean"], check=False)
# Warm caches once (untimed)
print(" Warming caches (not timed)...")
_ = run_hook(hook["config"], TARGET_FILES)
for i in range(REPEATS):
# Reset files to a known clean state before each timed run
subprocess.run(
["git", "restore", "--staged", "--worktree", "--source", "HEAD", "--", *TARGET_FILES],
check=False,
)
t = run_hook(hook["config"], TARGET_FILES)
print(f" Run {i + 1}: {t:.3f} seconds")
times.append(t)
results[hook["name"]] = times
return results
🧰 Tools
🪛 Ruff (0.12.2)

63-63: subprocess call: check for execution of untrusted input

(S603)


63-63: Consider ["git", "restore", *TARGET_FILES] instead of concatenation

Replace with ["git", "restore", *TARGET_FILES]

(RUF005)


64-64: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
In testing/benchmark_hooks.py around lines 56–70: move cache cleaning/warmup out of the per-run loop and ensure you restore both worktree and index to HEAD for deterministic state; specifically, for each hook run pre-commit clean once and perform a single “warm” invocation (to avoid measuring install/network time), then inside the REPEATS loop restore the repository to HEAD (resetting both index and worktree, e.g. git reset --hard HEAD) before timing each run; remove the per-iteration pre-commit clean so only the warming step cleans caches once per hook. 

def report(results):
lines = []
for name, times in results.items():
avg = statistics.mean(times)
std = statistics.stdev(times) if len(times) > 1 else 0.0
min_t = min(times)
max_t = max(times)
lines.append(
f"{name}: avg={avg:.3f}s, std={std:.3f}s, min={min_t:.3f}s, max={max_t:.3f}s, runs={len(times)}"
)
print("\nBenchmark Results:")
print("\n".join(lines))
with open(RESULTS_FILE, "w") as f:
f.write("\n".join(lines) + "\n")
print(f"Results saved to {RESULTS_FILE}")


def main():
results = benchmark()
report(results)


if __name__ == "__main__":
main()
Loading