Repository navigation
Compare PR benchmarks by instruction count - #1995
Merged
Merged
Conversation
The PR job compared one run against a stored baseline from another runner, so runner variance raised alerts on every PR. Commenting on those alerts failed on fork PRs with a read-only token, which turned the check red. Run base and head in turns on the same runner, compare the median ratio of mode times over three passes, and fail above 1.25x. Report through the step summary, which needs no write token. Skip PRs that touch no benchmarked code. 🤖 Generated with Claude Code
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The summary pipeline masks comparator failures, allowing detected performance regressions to pass the check.
1 open issue
What changed in this PR
This PR replaces cross-runner PR benchmark comparisons with paired base/head runs on the same runner, reducing noise while preserving master benchmark history.
Changes:
- Runs three alternating base/head passes and compares median timing ratios against a 1.25× threshold.
- Reports results in the step summary and limits PR runs to performance-related paths.
- Shares XML parsing between comparison and history conversion scripts.
| File | Description |
|---|---|
benchmarks/Utils/BenchmarkResults.php |
Extracts shared PHPBench XML parsing. |
benchmarks/convert-results.php |
Uses the shared parser for history output. |
benchmarks/compare-results.php |
Calculates median ratios and reports regressions. |
.github/workflows/benchmarks.yml |
Adds paired benchmark runs, path filtering, and summary output. |
Review effort: Balanced
Enable pipefail so the exit code of compare-results.php survives tee. Show PHPBench progress and add a job timeout, so a hang shows in the log. 🤖 Generated with Claude Code
runner.retry_threshold reruns iterations until their deviation is below 2%. PHPBench has no retry limit by default, so on noisy runners this can hang. Base runs with the config of head, so both sides measure the same way. 🤖 Generated with Claude Code
Wall times of identical code drifted by up to 43% between passes on hosted runners. Valgrind instruction counts of identical code match within 0.01% for most subjects, and reveal changes of below 1% that wall times hide. 🤖 Generated with Claude Code
count-instructions.php now lists subjects and picks both rev counts itself, so run-subject.php only runs one subject and prints nothing. A @revs that is not a single integer fails with a clear message. CONTRIBUTING.md explains how to reproduce the check locally. 🤖 Generated with Claude Code
A base count of zero or less now throws instead of passing as ratio 1. Benchmarks that exist only on base show as removed. The threshold is fixed at 1.1, since nothing passed another one. The summary warns that edited benchmarks compare different workloads. 🤖 Generated with Claude Code
…surements Seed the random generator before setup, so generated schemas match between runs. Fail when more revs count no more instructions, when @BeforeMethods names no method, or when base and head share no benchmark. Run commands without a shell and read only their error output. 🤖 Generated with Claude Code
🤖 Generated with Claude Code
The base SHA of the pull request event can lag behind the merge commit that the head checkout tests. 🤖 Generated with Claude Code
A probe run with 2 revs estimates the cost per rev, and the measured run adds enough revs for 20 million instructions, so the setup noise of tiny subjects no longer dominates their count. @revs no longer matters for the count. 🤖 Generated with Claude Code
🤖 Generated with Claude Code
Temporary, reverted in the next commit. 🤖 Generated with Claude Code
🤖 Generated with Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The benchmarks check fails on PRs that do not change performance, because wall times on hosted runners are noisy.
This PR counts instructions with Valgrind on base and head instead, and fails when head needs more than 1.1x.
The red check came from a failed PR comment, the alert from runner variance
On fork PRs,
GITHUB_TOKENis read-only, socomment-on-alertfailed with "Resource not accessible by integration", for example in https://github.com/webonyx/graphql-php/actions/runs/38578052847.The alert compared one run against a baseline from another runner at another time.
In that run, 30 of 31 benchmarks came out 1.75x to 2.5x slower by the same factor.
Instruction counts catch a regression and stay within 0.1% on identical code
In https://github.com/webonyx/graphql-php/actions/runs/40303300237, this PR changes no
src/code, and the largest ratio was 1.0007.A temporary
array_sum(range(1, 100))inLexer::advance()failed the check in https://github.com/webonyx/graphql-php/actions/runs/40303973119, with ratios of 1.35 and 1.31 forLexerBenchand 1.11 forBuildSchemaBench.An earlier version of this PR compared wall times of base and head on the same runner, and single passes of identical code drifted by up to 1.43x in https://github.com/webonyx/graphql-php/actions/runs/40290028062.
Each subject runs until it counts about 20 million extra instructions, so setup cost cancels out
benchmarks/run-subject.phpruns a PHPBench subject with its@BeforeMethods, outside of PHPBench.benchmarks/count-instructions.phpruns it undervalgrind --tool=cachegrindwith 1 rev, with 2 revs to estimate the cost per rev, and with enough revs to count about 20 million more instructions than the 1-rev run.The difference, divided by the extra revs, gives the instructions per rev.
A fixed share of
@Revswas not enough: the counts of whole processes vary by about 1,000 instructions, which madeScalarOverrideBench::benchGetTypeWithTypesOverride(about 2,300 per rev) fail with 1.79 on identical code in https://github.com/webonyx/graphql-php/actions/runs/40298556123.mt_srand(1)before setup makes the random schema ofHugeSchemaBenchthe same in every run.Opcache is off, so Valgrind sees all executed code.
Base is the first parent of the merge commit that GitHub checks out as head.
Base and head run in parallel, and the step takes about 6 to 8 minutes.
benchmarks/compare-results.phpwrites a table to the step summary, which needs no write token.Instruction counts miss cache misses and branch mispredictions, which matter little for this CPU-bound library.
Hardware counters through
perfare not available on Azure VMs: actions/runner-images#4974.PHPBench no longer retries iterations, because unlimited retries hung CI
runner.retry_threshold: 2reran iterations until their deviation was below 2%, and PHPBench has no retry limit by default.On noisy runners this hung
DeferredBench::benchManyNestedDeferredsfor 18 minutes in https://github.com/webonyx/graphql-php/actions/runs/40285990012, and probably killed the runner in https://github.com/webonyx/graphql-php/actions/runs/40214562986.doctrine/orm and phpactor set no retry threshold either.
Local
composer benchand thebenchmark-historyjob run without retries too, so their wall times may vary more."Count benchmark instructions" and the "Benchmarks" job in
test.ymlnow have a timeout.The job skips PRs that change no benchmarked code
A
pathsfilter limits it tosrc/,benchmarks/,composer.jsonand the workflow."Count benchmark instructions" is not a required check, so a skipped run blocks no merge.
CONTRIBUTING.mdexplains how to reproduce the check locally.Master history tracking still uses PHPBench wall times
The
benchmark-historyjob still pushes PHPBench results tobenchmark-results.Changelog
None
🤖 Generated with Claude Code