Bug report
Performance regression on the 2.2.x dev branch, found by auditing the bench.yml Test job history: analysing switch-heavy code that builds array shapes key by key (bench file tests/bench/data/bug-7581.php) got ~30 % slower on 2026-06-09, and this is the largest single unexplained share of why bug-7581.php currently fails the bench job in every run (currently +63–75 % over the committed baseline on slow runners, +27–40 % even on the fastest runner class).
When it started
The regression window is exactly the "reasons" batch merged between the 11:59 and 12:23 bench runs on 2026-06-09:
57ea87c835 Give reason for unresolvable PHPDoc types
a01080ece4 Explain unresolvable and impossible types with reasons
e5caec56fa Fold reasons tip into addTip closure
Evidence from the Test job (mode vs. the committed baseline.xml of 2026-05-27; both runs on same-speed runners — the unrelated borderline bench bug-9690.php reads +29.28 % / +28.99 % in the two runs, so runner speed is equal):
No other file moved between those two runs.
Bimodality, then pinned
Right after the batch the effect was bimodal: two fastest-class runners on effectively identical src gave +30.06 % (2026-06-09 19:32, Mo 511.8ms vs. Mo 393.5ms) and −0.05 % (2026-06-10 09:17) — there were no src commits between those runs. That looks like the file sits right at some cliff (union-size / degradation threshold) that run-to-run noise could still tip either way. After ad34ca7395 (phpstan/phpstan-src#5848, tracked separately) landed on 2026-06-11, it has been pinned on the slow side: 44 consecutive failing runs, never once passing on any runner class since.
In-process bench (median of 5 steady-state iterations, same setup as PHPStan\Benchmark\RegressionBench, PHP 8.5.5): with the later MutatingScope changes from phpstan/phpstan-src#5848 + phpstan/phpstan-src#5876 reverted on current 2.2.x HEAD, bug-7581.php improves 354 ms → 285 ms (−19 %), but is still ~+30 % above the scaled pre-June baseline — matching the share this batch introduced on CI before phpstan/phpstan-src#5848 existed.
The sample produces no errors; all the time goes into NodeScopeResolver/scope merging while $data grows a new constant-array-shape key per case, inside a foreach convergence loop over mixed. A plausible suspect is reason-string construction (AcceptsResult/IsSuperTypeOfResult reasons, the "impossible types" explanation path) now happening in hot paths even when the reasons are never displayed.
Reproduce
# in phpstan-src, 2.2.x
cp tests/bench/data/bug-7581.php test.php
bin/phpstan analyse -l 8 test.php --debug
# or the bench itself:
tests/vendor/bin/phpbench run --file=tests/bench/storage/baseline.xml --report=my-report
tests/bench/data/bug-7581.php (33-case switch building $data['A0']…$data['A32'])
<?php
namespace Bug7581;
/** @param mixed[] $parsed */
function parse(array $parsed): void
{
$data = [];
foreach ($parsed as $tag => $val):
switch ($tag):
case 'A0':
$data['A0'] = '';
break;
case 'A1':
$data['A1'] = '';
break;
// … identical cases A2 through A31 …
case 'A32':
$data['A32'] = '';
break;
endswitch;
endforeach;
echo 'test';
}
Code snippet that reproduces the problem
https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-7581.php
Expected output
Analysis time of bug-7581.php back near the committed CI baseline (Mo ≈ 394 ms on GitHub runners, i.e. within the ±25 % bench tolerance) with no change in reported errors. The reasons/tips machinery should be lazy or otherwise free when no error message ends up being rendered.
Related bench regressions keeping the Test job red since 2026-06-11 (this file was later pinned permanently red by the first one): #14920 (phpstan-src#5848 guard check), #14921 (phpstan-src#5876 conditional targets), #14919 (ExprHandler dispatch refactor, same week).
Bug report
Performance regression on the 2.2.x dev branch, found by auditing the bench.yml Test job history: analysing switch-heavy code that builds array shapes key by key (bench file
tests/bench/data/bug-7581.php) got ~30 % slower on 2026-06-09, and this is the largest single unexplained share of whybug-7581.phpcurrently fails the bench job in every run (currently +63–75 % over the committed baseline on slow runners, +27–40 % even on the fastest runner class).When it started
The regression window is exactly the "reasons" batch merged between the 11:59 and 12:23 bench runs on 2026-06-09:
57ea87c835Give reason for unresolvable PHPDoc typesa01080ece4Explain unresolvable and impossible types with reasonse5caec56faFold reasons tip into addTip closureEvidence from the Test job (mode vs. the committed
baseline.xmlof 2026-05-27; both runs on same-speed runners — the unrelated borderline benchbug-9690.phpreads +29.28 % / +28.99 % in the two runs, so runner speed is equal):a0d97202d5e5caec56fa(includes the whole batch)No other file moved between those two runs.
Bimodality, then pinned
Right after the batch the effect was bimodal: two fastest-class runners on effectively identical src gave +30.06 % (2026-06-09 19:32,
Mo 511.8ms vs. Mo 393.5ms) and −0.05 % (2026-06-10 09:17) — there were no src commits between those runs. That looks like the file sits right at some cliff (union-size / degradation threshold) that run-to-run noise could still tip either way. Afterad34ca7395(phpstan/phpstan-src#5848, tracked separately) landed on 2026-06-11, it has been pinned on the slow side: 44 consecutive failing runs, never once passing on any runner class since.Local confirmation that this share is separate from phpstan/phpstan-src#5848/phpstan/phpstan-src#5876
In-process bench (median of 5 steady-state iterations, same setup as
PHPStan\Benchmark\RegressionBench, PHP 8.5.5): with the laterMutatingScopechanges from phpstan/phpstan-src#5848 + phpstan/phpstan-src#5876 reverted on current 2.2.x HEAD,bug-7581.phpimproves 354 ms → 285 ms (−19 %), but is still ~+30 % above the scaled pre-June baseline — matching the share this batch introduced on CI before phpstan/phpstan-src#5848 existed.The sample produces no errors; all the time goes into NodeScopeResolver/scope merging while
$datagrows a new constant-array-shape key percase, inside aforeachconvergence loop overmixed. A plausible suspect is reason-string construction (AcceptsResult/IsSuperTypeOfResultreasons, the "impossible types" explanation path) now happening in hot paths even when the reasons are never displayed.Reproduce
tests/bench/data/bug-7581.php (33-case switch building $data['A0']…$data['A32'])
Code snippet that reproduces the problem
https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-7581.php
Expected output
Analysis time of
bug-7581.phpback near the committed CI baseline (Mo ≈ 394 mson GitHub runners, i.e. within the ±25 % bench tolerance) with no change in reported errors. The reasons/tips machinery should be lazy or otherwise free when no error message ends up being rendered.Related bench regressions keeping the Test job red since 2026-06-11 (this file was later pinned permanently red by the first one): #14920 (phpstan-src#5848 guard check), #14921 (phpstan-src#5876 conditional targets), #14919 (ExprHandler dispatch refactor, same week).