Skip to content

Commit 3d064dc

Browse files
committed
perf(ci): add baseline regression gate for pull requests
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 9ff8af0 commit 3d064dc

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

.github/.performance/baseline.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
22
"version": "1.0.0",
33
"created_at": "2026-06-01",
4+
"allowed_regression_pct": 25.0,
45
"benchmarks": {
56
"LibreSign\\XObjectTemplate\\Benchmarks\\CompilerBench::benchSimpleHtml": {
6-
"mean": 2.5,
7+
"mean": 0.356085,
78
"memory_real": 768
89
},
910
"LibreSign\\XObjectTemplate\\Benchmarks\\CompilerBench::benchComplexHtml": {
10-
"mean": 3.2,
11+
"mean": 1.366,
1112
"memory_real": 1024
1213
}
1314
}

.github/copilot-instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ PHPBench automatically:
2929
- Executes multiple revisions for statistical confidence
3030
- Reports mean, min, max, stdev, variance per benchmark (CI dump file: `build/benchmark-results.xml`)
3131

32+
Baseline persistence policy:
33+
- Regression gate compares PR results against `.github/.performance/baseline.json`.
34+
- Baseline updates are made via pull request (no direct commit to protected `main`).
35+
3236
## Compliance and contribution
3337

3438
- DCO sign-off is mandatory for every commit.

.github/workflows/performance.yml

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,83 @@ jobs:
2525
- uses: shivammathur/setup-php@v2
2626
with:
2727
php-version: ${{ steps.php_min.outputs.version }}
28+
coverage: none
2829
- run: composer install --no-interaction --prefer-dist
2930
- run: composer bin all install --no-interaction --prefer-dist
3031

3132
- name: Run benchmarks (strict CI mode with verbose output)
3233
run: |
33-
composer benchmark:run:ci
34+
composer benchmark:run:ci | tee build/benchmark-output.txt
3435
3536
echo "✓ Benchmarks completed"
3637
echo ""
3738
echo "Results saved to build/benchmark-results.xml"
3839
echo "Review: Ensure no individual benchmark takes >10ms average"
40+
41+
- name: Validate regression against baseline
42+
if: github.event_name == 'pull_request'
43+
run: |
44+
python3 - <<'PY'
45+
import json
46+
import pathlib
47+
import re
48+
import sys
49+
50+
baseline_path = pathlib.Path('.github/.performance/baseline.json')
51+
output_path = pathlib.Path('build/benchmark-output.txt')
52+
53+
if not baseline_path.exists():
54+
print('Baseline file not found, skipping regression validation.')
55+
sys.exit(0)
56+
57+
if not output_path.exists():
58+
print('Benchmark output not found, cannot validate regression.')
59+
sys.exit(1)
60+
61+
baseline = json.loads(baseline_path.read_text())
62+
output = output_path.read_text()
63+
64+
patterns = {
65+
'LibreSign\\XObjectTemplate\\Benchmarks\\CompilerBench::benchSimpleHtml': r'benchSimpleHtml.*?Mo([0-9]+(?:\\.[0-9]+)?)(μs|us|ms)',
66+
'LibreSign\\XObjectTemplate\\Benchmarks\\CompilerBench::benchComplexHtml': r'benchComplexHtml.*?Mo([0-9]+(?:\\.[0-9]+)?)(μs|us|ms)',
67+
}
68+
69+
tolerance = float(baseline.get('allowed_regression_pct', 25.0)) / 100.0
70+
benchmark_baseline = baseline.get('benchmarks', {})
71+
72+
failures = []
73+
for name, pattern in patterns.items():
74+
match = re.search(pattern, output)
75+
if not match:
76+
failures.append(f'{name}: metric not found in benchmark output')
77+
continue
78+
79+
value = float(match.group(1))
80+
unit = match.group(2)
81+
current_ms = value / 1000.0 if unit in ('μs', 'us') else value
82+
83+
if name not in benchmark_baseline:
84+
failures.append(f'{name}: missing baseline entry')
85+
continue
86+
87+
baseline_ms = float(benchmark_baseline[name]['mean'])
88+
limit_ms = baseline_ms * (1.0 + tolerance)
89+
90+
print(f'{name}: current={current_ms:.6f}ms baseline={baseline_ms:.6f}ms limit={limit_ms:.6f}ms')
91+
92+
if current_ms > limit_ms:
93+
failures.append(
94+
f'{name}: regression detected ({current_ms:.6f}ms > {limit_ms:.6f}ms)'
95+
)
96+
97+
if failures:
98+
print('\nPerformance regression gate failed:')
99+
for failure in failures:
100+
print(f' - {failure}')
101+
sys.exit(1)
102+
103+
print('\nPerformance regression gate passed.')
104+
PY
39105
40106
- name: Save benchmark results as artifact
41107
if: always()

0 commit comments

Comments
 (0)