Skip to content

Let another program that loads the phar use PHPStan Turbo - #6720

Merged
ondrejmirtes merged 1 commit into
phpstan:2.3.xfrom
SanderMuller:turbo-host-phar-path
Oct 17, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.3.xfrom
SanderMuller:turbo-host-phar-path

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

Rector loads PHPStan's phar from vendor/phpstan/phpstan, and it gets a lot faster with PHPStan Turbo. On rector-src with its own rector.php, Rector takes 7.9 s instead of 11.8 s, and 60 s of CPU instead of 98 s. Those are medians of 5 cold runs with 14 workers, on an M4 Pro with PHP 8.5. Rector calls the API below, with a phar built from this branch. The output is the same with and without Turbo.

To get there, Rector needs the same three calls as bin/phpstan. But these calls find the phar through Phar::running(), which is empty when Rector is the script that runs.

This adds an optional ?string $pharPath to TurboProcessRestarter::restartIfSuitable(), TurboExtensionEnabler::trustOwnTypesIfSuitable() and TurboExtensionEnabler::activateIfCompatible(), and to TurboExtensionSelector::findExtension() and findExtensionForWorkers(), which they call. Without the argument, nothing changes. The three methods that another program calls are now @api. A path that is passed in goes through realpath(). Code compiled from the phar carries the resolved path that Composer's bootstrap.php opens it with. Without realpath(), a path like bin/../vendor/... switches trusted types on, but they have no effect. tests/bootstrap.php calls activateIfCompatible(), so its phpstanApi.method ignore is not needed any more.

Rector would call them right after its Composer autoloader:

$phpstanPhar = __DIR__ . '/../vendor/phpstan/phpstan/phpstan.phar';
TurboProcessRestarter::restartIfSuitable($_SERVER['argv'], $phpstanPhar);
TurboExtensionEnabler::trustOwnTypesIfSuitable($_SERVER['argv'], $phpstanPhar);
TurboExtensionEnabler::activateIfCompatible($phpstanPhar);

Its workers start as new processes, so each of them restarts once in the same way. I checked the main process and all 14 workers. In each one the extension is active and OPcache is on. A class-typed parameter check in the phar is dropped, the same as inside PHPStan. With Rector's --debug, the checks stay, like with PHPStan's.

Rector needs two changes of its own. One is merged: rectorphp/rector-src#8564 removes extra arguments to the TypeTraverser callback, which the native callback rejects. The other is still to do: Rector's preload.php must skip PhpParser\NodeTraverser when Turbo has already declared it.

make tests and make phpstan pass, and phpcs reports no errors on the changed files.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ondrejmirtes
ondrejmirtes merged commit 807d030 into phpstan:2.3.x Oct 17, 2026
963 of 973 checks passed
@ondrejmirtes

Copy link
Copy Markdown
Member

Thank you! Sorry for keeping you wait for a week. You can now resume sending improvements to Rector 😊 Because this needs unreleased ^2.3.2, maybe do a single branch with proof of concept of enabling Turbo all the way in Rector and have actual measurements in hand.

Of course if there's more to be donr in Rector without this API, could be done as preparation work before without needing ^2.3.2.

@SanderMuller

Copy link
Copy Markdown
Contributor Author

Thanks. The proof of concept is on https://github.com/SanderMuller/rector-src/tree/turbo-poc. It requires phpstan/phpstan 2.3.x-dev, which already has the phar for 807d030 and matching Turbo binaries.

It has two commits:

On rector-src itself, a cold bin/rector process --dry-run --clear-cache with 14 workers, 5 interleaved runs per side, on an M4 Pro with PHP 8.5:

wall CPU
without Turbo 11.6 s (10.7 to 19.3) 99.6 s (98.7 to 121.5)
with Turbo 8.2 s (7.8 to 12.5) 60.8 s (59.8 to 62.1)

These are medians, with the range in brackets. The output is the same in all 10 runs. In a separate run, a probe showed Turbo active, trusted types on and OPcache on in the main process and all 14 workers.

Two things are still open. The branch only handles rector-src's own layout: when Rector is installed as a dependency, PHPStan is loaded with the project's autoloader, after preload.php. And Rector's workers are spawned, so each one starts from scratch. Forked workers would be the next step.

@SanderMuller

Copy link
Copy Markdown
Contributor Author

Numbers on a bigger project. I ran the PoC on Shopware v6.7.6.2 with its own rector.php, about 9,200 files under src and tests. Each arm ran 5 interleaved cold process --dry-run --clear-cache runs:

wall CPU
without Turbo 34.9 s (32.0 to 35.7) 310.5 s (303.6 to 314.0)
with Turbo 20.5 s (19.3 to 21.5) 160.4 s (160.2 to 170.8)

These are medians, with the range in brackets. All 10 runs change the same 14 files with the same diffs.

One correction to my rector-src numbers above. The base arm there was two upstream Rector commits older than the PoC, and one of them is the perf change rectorphp/rector-src#8687. Now the PoC is the only difference between the arms. With that base, rector-src goes from 11.9 s to 8.7 s, and from 99.5 s to 61.6 s of CPU. So the earlier result holds.

Two notes on the Shopware run. Its rector.php points to an old container XML path, so I used a copy with the current path. Shopware's vendor has its own php-parser and symfony/console, so I loaded its autoloader after Rector's. In a separate run, a probe confirmed that Rector's copies loaded, and that Turbo, trusted types and OPcache were on in every process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants