From Minutes to Seconds: Massive Performance Gains in PHPStan
March 1, 2020 · 5 min read
Strongly-typed object-oriented code helps me tremendously during refactoring. When I realize I need to pass more information from one place to another, I usually take my first step by changing a return typehint, or adding a required parameter to a method. Running PHPStan after making these changes tells me about all the code I need to fix. So I run it many times an hour during my workday. I don’t even check the web app in the browser until PHPStan’s result is green.
To encourage these types of workflows and to generally speed up feedback, I made massive improvements to its performance in the latest release. You can now run PHPStan comfortably as part of Git hooks or manually on your machine, not just in CI pipelines when you decide to submit your branch for a code review at the end of the day. Even on huge codebases consisting of hundreds of thousands lines of code, it finishes the analysis in seconds!
How was this achieved?
Parallel analysis #
PHPStan now runs in multiple processes by default. The list of files to analyse is divided into small chunks which get processed by the same number of processes as the number of CPU cores you have. It works on all operating systems and doesn’t require any special PHP extension. And it’s enabled by default.
This feature was first released three weeks ago under an optional feature flag users had to turn on. The initial response from the early adopters was overwhelmingly positive:
That's awesome!— what does trump mean when he says words? (@alex_schnitzler) February 14, 2020
Runs smooth and uses all 16 cores.
Analysis of 2357 files in 12 seconds. Took minutes before.
In a VM:— Hugo Alliaume (Kocal) ☣️ (@HugoAlliaume) February 14, 2020
- app1: from ~22sec to ~12sec
- app2: from ~7sec to ~4sec
In GitHub Actions CI (we run Symfony `bin/console cache:clear` and PHPStan in the same step 😟):
- app1: from ~43sec to ~26sec
- app2: from ~13sec to ~10sec
that's nice :)
Pretty awesome! Thanks! roughly down from 15 to 5 minutes. 🥳— Christopher Hertel (@el_stoffel) February 13, 2020
Well done!!! It’s 4-times faster now!!!— Lukáš Břečka (@ftipalek) February 13, 2020
analysis takes 15 seconds instead of 1 minute with previous version.
Result cache #
Even with the improvements achieved by parallel analysis, there was one more opportunity to achieve an order-of-magnitude speed-up.
One idea that’s easy to come up with is to analyse only changed files. You might have seen a suggestion to run PHPStan like this:
# Don't do this!
git diff --name-only origin/master..HEAD -- *.php | xargs vendor/bin/phpstan analyse
First problem with this approach is that by changing one file, we can break a different file. For example if we add a method to an interface we need to reanalyse all the classes that implement the interface. We might miss out on real errors with this naïve approach.
Instead, we need to build a tree of dependencies between the files. So when we detect that
A.php was changed, we analyse
A.php+ all the files calling or otherwise referencing all the symbols in
Second problem is that even if we successfully mark all the right files that need to be reanalysed, we won’t see the errors from the remaining files. In order for users to not even notice they’re really analysing only a subset of files, we need to cache the list of errors from past analyses.
Hence the name: result cache.
The cache gets invalidated, and PHPStan runs on all files again, if one or more of these items changed since the most recent analysis:
- PHPStan version
- PHP version
- Loaded PHP extensions
- composer.lock contents (3ʳᵈ party dependencies were updated)
- Contents of the project’s PHPStan configuration file
- Contents of included stub files for overriding 3ʳᵈ party PHPDocs
- Used rule level (using
-lon the command line)
- Analysed paths
Especially the last item now makes the practice using
git diff counter-productive. The list of analysed paths needs to remain stable for the result cache to be used. So the best practice is to always run PHPStan on the whole project.
If you want to run PHPStan without the result cache being used for some reason, you can use the new
vendor/bin/phpstan clear-result-cache -c phpstan.neon && \
vendor/bin/phpstan analyse -c phpstan.neon -l 8 src/ tests/
I’m really excited about these improvements and can’t wait till you try them out! I’m looking forward to your feedback.