Bug report
Performance regression on the 2.2.x dev branch, found by auditing the bench.yml Test job history. Since ad34ca7395 (phpstan/phpstan-src#5848, Skip conditional expression guard when guard type is a subtype of the other branch's value) landed on 2026-06-11, the bench Test job has failed on every single full run (previously it was intermittently green depending on runner speed; the last full green run is 2026-06-10, the first red one is 2026-06-11, and ad34ca7395 is the only src/ commit between them).
The change is 2 lines in MutatingScope (conditional-expression guard selection during branch merging):
$guardIsSuperTypeOfTheirExpr = $guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType());
+$theirExprIsSuperTypeOfGuard = $theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType());
if (
$guardIsSuperTypeOfTheirExpr->yes()
+ || $theirExprIsSuperTypeOfGuard->yes()
The extra reverse isSuperTypeOf() runs per guard holder per branch merge, and in condition-heavy code with big union / constant-array types that is a hot path.
Per-file impact (mode vs. the committed baseline.xml of 2026-05-27, comparable runner classes)
| bench |
baseline |
tolerance |
before (Jun 9) |
after (Jun 11) |
now (Jul 4) |
| bug-10538.php |
2.61 s |
±10 % |
+11.8 % |
+25.9 % |
+33–44 % (further step from phpstan/phpstan-src#5876, tracked separately) |
| bug-14462.php |
158 ms |
±25 % |
+13.1 % |
+39.2 % |
+26–49 % |
| bug-13352.php |
3.38 s |
±10 % |
+8.7 % |
+14.1 % |
+17–24 % on slower runners |
| bug-11283.php |
~883 ms |
±20 % |
passing |
+20.3 % |
fails on slowest runners |
| bug-7581.php |
394 ms |
±25 % |
+30 % intermittent (separate June 9 regression) |
+73.9 %, permanently red |
+63–75 % |
bug-7581.php had been flirting with the threshold since the June 9 "reasons" batch (tracked separately), but this commit pinned it: 44 consecutive failing runs since, on every runner class.
Local confirmation
In-process bench on current 2.2.x HEAD (median of 5 steady-state iterations, same setup as PHPStan\Benchmark\RegressionBench, PHP 8.5.5), reverting only the 2 lines above:
| bench |
HEAD |
2 lines reverted |
| bug-7581.php |
354.3 ms |
278.9 ms (−21 %) |
| bug-10538.php |
2 037.5 ms |
1 774.5 ms (−13 %) |
| bug-14462.php |
114.9 ms |
95.4 ms (−17 %) |
There is also an interaction with the later c53a7b8a5f (phpstan/phpstan-src#5876): on bug-14462.php, reverting phpstan/phpstan-src#5876 alone changes nothing, but reverting both recovers −21 % — the two changes compound in the same guard machinery.
Reproduce
# in phpstan-src, 2.2.x
cp tests/bench/data/bug-14462.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-14462.php
<?php declare(strict_types = 1);
namespace Bug14462;
/** @return array{menu: array<non-empty-string, bool>} */
function get_config(): array {
return ['menu' => []];
}
$config = get_config();
$data = [ ];
if ($config['menu']['notefrais']) {
$data[] = [ 'name' => 'notefrais', 'menu' => 'notefrais_base' ];
}
if ($config['menu']['achat']) {
$data[] = [ 'name' => 'achat', 'menu' => 'achat_base' ];
}
if ($config['menu']['vente-commande_planning'] || $config['menu']['vente-commande']) {
$data[] = [ 'name' => 'vente' , 'menu' => 'vente_order_recent' ];
}
if ($config['menu']['vente-commande_planning']) {
$data[] = [ 'name' => 'vente', 'menu' => 'vente_base_planned' ];
}
if ($config['menu']['vente-commande']) {
$data[] = [ 'name' => 'vente', 'menu' => 'vente_base_com' ];
}
if ($config['menu']['carte']) {
$data[] = [ 'name' => 'carte', 'menu' => '' ];
}
if ($config['menu']['crm']) {
$data[] = [ 'name' => 'crm', 'menu' => 'crm_suivi' ];
}
if ($config['menu']['inventaire']) {
$data[] = [ 'name' => 'inventaire', 'menu' => 'inventaire_base' ];
}
foreach ($data as $row) {
$stack = [ ];
if ($row['menu'] === 'vente_order_recent') {
$stack[] = 'f';
}
else {
$stack[] = 'g';
}
}
tests/bench/data/bug-10538.php (nested foreach over a ~100-entry constant array of string array shapes) is the other main victim: https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-10538.php
Code snippet that reproduces the problem
https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-14462.php
Expected output
The false-positive fix from phpstan/phpstan-src#5848 kept, but without the 15–25 % analysis-time cost on condition-heavy code — e.g. by short-circuiting the reverse isSuperTypeOf() check (it is only needed when the forward check didn't already decide), caching it, or deriving the answer from information the merge already has. Bench job green again on typical runners.
Related bench regressions keeping the Test job red since 2026-06-11: #14921 (phpstan-src#5876 — compounds with this change on bug-14462.php), #14918 (type "reasons" commits — the earlier bug-7581.php share), #14919 (ExprHandler dispatch refactor — bug-14207-and.php / and-chain-truthy-blowup.php).
Bug report
Performance regression on the 2.2.x dev branch, found by auditing the bench.yml Test job history. Since
ad34ca7395(phpstan/phpstan-src#5848, Skip conditional expression guard when guard type is a subtype of the other branch's value) landed on 2026-06-11, the bench Test job has failed on every single full run (previously it was intermittently green depending on runner speed; the last full green run is 2026-06-10, the first red one is 2026-06-11, andad34ca7395is the onlysrc/commit between them).The change is 2 lines in
MutatingScope(conditional-expression guard selection during branch merging):The extra reverse
isSuperTypeOf()runs per guard holder per branch merge, and in condition-heavy code with big union / constant-array types that is a hot path.Per-file impact (mode vs. the committed
baseline.xmlof 2026-05-27, comparable runner classes)bug-7581.phphad been flirting with the threshold since the June 9 "reasons" batch (tracked separately), but this commit pinned it: 44 consecutive failing runs since, on every runner class.Local confirmation
In-process bench on current 2.2.x HEAD (median of 5 steady-state iterations, same setup as
PHPStan\Benchmark\RegressionBench, PHP 8.5.5), reverting only the 2 lines above:There is also an interaction with the later
c53a7b8a5f(phpstan/phpstan-src#5876): onbug-14462.php, reverting phpstan/phpstan-src#5876 alone changes nothing, but reverting both recovers −21 % — the two changes compound in the same guard machinery.Reproduce
tests/bench/data/bug-14462.php
tests/bench/data/bug-10538.php(nestedforeachover a ~100-entry constant array of string array shapes) is the other main victim: https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-10538.phpCode snippet that reproduces the problem
https://github.com/phpstan/phpstan-src/blob/2.2.x/tests/bench/data/bug-14462.php
Expected output
The false-positive fix from phpstan/phpstan-src#5848 kept, but without the 15–25 % analysis-time cost on condition-heavy code — e.g. by short-circuiting the reverse
isSuperTypeOf()check (it is only needed when the forward check didn't already decide), caching it, or deriving the answer from information the merge already has. Bench job green again on typical runners.Related bench regressions keeping the Test job red since 2026-06-11: #14921 (phpstan-src#5876 — compounds with this change on bug-14462.php), #14918 (type "reasons" commits — the earlier bug-7581.php share), #14919 (ExprHandler dispatch refactor — bug-14207-and.php / and-chain-truthy-blowup.php).