Skip to content

Speed up single-file and editor-mode runs: skip unsaved exported nodes and OPcache that can't pay off - #6988

Open
ondrejmirtes wants to merge 3 commits into
2.3.xfrom
editor-mode-startup
Open

ondrejmirtes wants to merge 3 commits into
2.3.xfrom
editor-mode-startup

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Single-file and editor-mode runs got about twice as slow after 2.2.13. There are two causes, one per commit.

Exported nodes fetched for a cache that is never saved. Since 3eb12b6, the full-analysis save callback in ResultCacheManager::process() called addNonAnalysedExportedNodes() before doSave() checked whether it saves anything. A run on files named on the command line, or in editor mode, never saves, so it parsed every non-analysed file the analysed code depends on for nothing. doSave() now receives the nodes as a callback and calls it only after the "cache won't be saved" checks. On WordPress src/index.php, which requires 4 files: 0.98 s → 0.70 s with --debug PHPSTAN_RESTART=none.

OPcache activated where it can't pay off. Compiling PHPStan into OPcache costs about 0.45 s per process, mostly the optimizer. The restart and the spawned workers paid that on every run, even a one-file run:

  • The restart no longer turns OPcache on in the main process when the workers will be spawned (a phar with no turbo extension next to it). Only forked workers reuse what the main process compiled. With spawned workers, the main process just boots and collects results, and it was slower with OPcache at every size I measured (263 WordPress files: 22.6 s with OPcache vs 21.7 s without).
  • Nor when the command line names at most OpcacheSettings::FEW_FILES_LIMIT (10) files and no directory, e.g. analyse src/Foo.php, the way editors run PHPStan. The restart still happens for the other reasons (turbo, xdebug, pcov, uopz).
  • Spawned analysis workers get OPcache only when there are more than 10 files to analyse. This also covers incremental runs that re-analyse only a few files.

Break-even, bare phar with spawned workers, WordPress wp-includes files (heavy ones): OPcache costs 0.4 s for 1 file, 0.35 s for 5 files, and gains 0.24 s at 20 files (9.4 s total). So 10 is a conservative limit.

Large runs are not slower. All of src/wp-includes/*.php (263 files, best of 2): bare phar 25.5 s → 21.5 s, with turbo 12.7 s → 11.1 s. The first commit accounts for part of that, since a run on named files never saves the cache.

Wall time, best of 5 (one warm-up run discarded)

2.1.34 2.3.0 2.3.x-dev this PR
wordpress-develop analyse src/index.php 1.30 s 2.36 s 2.46 s 1.30 s
trivial file analyse -l 5 test.php 0.74 s 1.62 s 1.76 s 0.89 s
wordpress-develop src/index.php, turbo binary next to the phar – – 1.30 s 0.87 s
trivial file, turbo binary next to the phar – – 0.85 s 0.54 s

Setup: PHP 8.4.27 NTS on Linux x86_64 with 16 cores, OPcache loaded but dormant on CLI (the default). 2.1.34 and 2.3.0 are the release PHARs. 2.3.x-dev (8af6dd7) and this PR were built the same way (compiler/bin/prepare + box + resign.php, as in phar.yml). The first two rows use bare PHARs with no turbo binary, so their workers are spawned. The turbo rows put a locally built phpstan_turbo.so next to the PHAR, which is how the Composer package ships it, so their workers are forked. wordpress-develop uses its own phpstan.neon.dist (level 5). For 2.1.34 it ran in a copy with phpstan-phpunit 2.0.12 / phpstan-strict-rules 2.0.7 and phpVersion.max: 80599, because the current versions require 2.3.

Tests

  • RestartManagerTest: the command-line heuristic (option values, --, directories, the limit, --watch/--pro, other commands).
  • e2e/phar-run now analyses 11 files, so the phar-run jobs keep asserting that the spawned workers get OPcache. A new restart-manager e2e row (e2e-tests.yml) asserts that a single file analysed with OPcache dormant doesn't restart and that neither a forked nor a spawned worker runs with OPcache; it fails on the base commit.
  • bug-15184 / bootstrap-autoloader-wrong-file: their spawned-worker runs now get OPcache from the php.ini (PHP_INI_SCAN_DIR). Without that, these one-file runs would no longer have OPcache and would pass whether or not the bug is fixed. I checked locally that the step still reproduces the crash once invalidateIncludedFiles() is disabled.
  • Locally: make phpstan, make cs, tests/PHPStan/{Analyser/ResultCache,Process,Parallel,Command,Build/PreAutoloadFilesTest}, and the restart-manager e2e opcache and --debug rows.

…ache is saved

The full-analysis save callback fetched them before doSave() decided not to
save at all - a run on files passed on the command line or in editor mode
never saves - so every such run parsed each file the analysed code depends on
for nothing. doSave() now receives them as a callback and calls it only once
the cache is known to be saved.

WordPress src/index.php, which requires four files: 0.98 s -> 0.70 s with
--debug and PHPSTAN_RESTART=none.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b250-30d4-72bb-8d3e-b0687619f435
Compiling PHPStan into OPcache costs about half a second per process, mostly
the optimizer, which only a long enough analysis earns back. Since the restart
activated it, a single-file run paid that twice - in the main process and in
the spawned worker - and took twice as long as before.

- The restart no longer activates OPcache in the main process when the
  workers will be spawned (a phar without the turbo extension): only forked
  workers use what the main process compiled, otherwise it just boots and
  collects results, and was measured slower with OPcache at every size.
- Nor when the command line names at most OpcacheSettings::FEW_FILES_LIMIT
  files and nothing else to analyse - `analyse src/Foo.php`, the way editors
  run PHPStan. The restart still happens for the other reasons (turbo,
  xdebug, ...).
- Spawned analysis workers get the OPcache entries only when there are more
  than that many files to analyse - which also covers an incremental run
  re-analysing a few files.

e2e/phar-run analyses 11 files so its workers keep asserting the OPcache they
get, and a new phar-run step asserts a single-file run neither restarts nor
spawns its worker with OPcache. The bug-15184 and
bootstrap-autoloader-wrong-file spawned runs get OPcache from the php.ini,
the crash they guard against needs it.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b250-30d4-72bb-8d3e-b0687619f435
…ests

A change to phar.yml makes the PHAR checksum and prefix-diff jobs compare the
PR's PHAR with the base one, which only a compiler-only change passes. The
restart-manager row checks the same from source and also covers forked
workers: a single file analysed with OPcache dormant restarts nothing, and
neither the forked nor the spawned worker runs with OPcache. It fails on the
base commit.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b250-30d4-72bb-8d3e-b0687619f435
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.

1 participant