Skip to content

Skip the get_included_files() scan in the autoload trap while OPcache compiled nothing - #6979

Open
ondrejmirtes wants to merge 1 commit into
2.3.xfrom
fix-autoload-trap-invalidation
Open

ondrejmirtes wants to merge 1 commit into
2.3.xfrom
fix-autoload-trap-invalidation

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

#6742 made AutoloadSourceLocator::invalidateIncludedFiles() call get_included_files() before every autoloader that runs in the autoload trap. That call copies every included path, so each class lookup that reaches a non-Composer autoloader costs time proportional to the number of included files. PHPStan 2.3 restarts itself with OPcache on, so this path is active by default. A project bootstrap, such as a Symfony kernel boot, includes thousands of files.

The invalidation is only needed after something got compiled into OPcache since the last scan. With this change, invalidateIncludedFiles() reads OPcache's hits and misses from opcache_get_status(false), which costs about 1 µs against about 55 µs for get_included_files() with 10,000 included files. When the counters equal the ones from its last scan, it skips the scan. A script gets into OPcache's shared memory, or runs from it, only through a miss or a hit, so equal counters mean there is nothing new to invalidate. The existing cursor still makes sure each included file is invalidated at most once.

After the trap, the loop that invalidates the trap's empty scripts now also counts those compiles as handled, provided there was no hit in between. OPcache caches only file:// and phar:// paths, and the trap overrides both, so every miss inside the trap is one of its own scripts. Without this, every class that a trapped autoloader finds would cause a scan at the next lookup.

When the counters are not available, it scans as before. That is the case with opcache.restrict_api, with opcache.file_cache_only, and with opcache.file_cache configured, because a script loaded from the file cache counts as neither a hit nor a miss. PHPStan's restart blanks opcache.file_cache.

e2e/bootstrap-autoloader-wrong-file now first looks up a class that has no file, which runs a scan, and then the class whose file the bootstrap autoloader includes for real outside the trap. If the counter check skipped scans after the first one, this would die with "Cannot redeclare class". I checked that: with that mutation the new test crashes, and the old test still passes.

Measurements

Reproducer, level 0: bootstrapFiles: [bootstrap.php]. bootstrap.php registers spl_autoload_register(static function (string $class): void {}); and require_onces inc/f0.php … inc/f9999.php, each declaring one function. test.php is one class with 1,000 methods public function mN(): void { new \Missing\ClassN(); }.

The numbers are from analyse --debug -vvv: the per-file took time and the total peak memory, best of 5 runs, on PHP 8.4.27 without the Turbo extension.

2.1.34 2.3.0 2.3.x-dev this PR
Time per file (best of 5) 0.93 s 0.82 s 1.30 s 1.05 s
Peak memory 123.05 MB 89.77 MB 54 MB 46 MB
  • 2.1.34 (release PHAR) does not restart, and OPcache stays off on CLI.
  • 2.3.0 (release PHAR), 2.3.x-dev (8af6dd7) and this PR restart with OPcache on (opcache.enable_cli=1, validate_timestamps=0, file_cache=, JIT off). --debug runs the analysis in that main process, so the trap runs with OPcache active.
  • 2.3.x-dev and this PR run from a source checkout (bin/phpstan with the same vendor/), not from a PHAR, so compare them with each other rather than with the release PHARs.
  • With invalidateIncludedFiles() removed entirely, the same checkout measured 1.01–1.06 s, the same as this PR. In this run, this PR scans get_included_files() twice instead of once per lookup.

… compiled nothing

invalidateIncludedFiles() copied every included path before each autoloader
that runs in the trap, so the cost of a class lookup grew with the number of
files a bootstrap included. It now compares OPcache's hits and misses with the
ones it saw at its last scan and skips the scan when they are equal. The trap's
own compiles, which the loop after the trap invalidates, do not count.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b250-1b0b-7741-b14c-5cf983afb658
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