Answer reflection cache misses without a failing include() - #6430
Merged
Conversation
FileCacheStorage::load() learned that an entry was never written by letting `@include` fail, which costs about 29us per miss: PHP walks the include_path and builds two warnings only for the @ to discard them. A stat() rules the file out in about 1.4us and adds nothing to the hit path, where the include stats the file anyway. Misses are far from rare. CachedPhpInternalSourceLocator sits behind every file locator, so every class name they fail to resolve asks the cache whether it is a PHP built-in - 114k misses per run on a large project, in every run, warm cache or not. The stubs map is the only source the wrapped locator reads from, so a class it does not list can never have been cached; checking the map first skips the key hashing and the stat() too. The wrapped locator is still asked, so class alias resolution behaves as before. Measured on a large project (10 workers, full analysis): the cache-miss path dropped from 5.0s to 1.95s of fleet time with the guard alone, and the whole run by 2.3s of user CPU across four ABBA pairs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2cdRMUX8Be44eUzEM6RQ2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FileCacheStorage::load()found out that an entry was never written by letting@includefail. That costs about 29 µs per miss: PHP walks the include_path and builds two warnings, and then the@throws them away. Astat()rules the file out in about 1.4 µs, and it's free on the hit path because the include stats the file anyway (90.0 vs 89.9 µs per hit, microbenchmarked).Misses are common.
CachedPhpInternalSourceLocatorsits behind every file locator, so every class name those locators can't resolve asks the cache whether it's a PHP built-in. On a large project (slevomat, 10 workers, full analysis) that's ~114k misses per run, in every run, whether the cache is warm or cold. The wrapped locator answers "not a built-in" in 3.8 µs, so the cache in front of it cost 8–12x more than the work it was caching.Two changes:
is_file()guard inFileCacheStorage::load(). This is general and helps every miss path.CachedPhpInternalSourceLocator. The stubber is the only source the wrapped locator reads from, so a class not listed inPhpStormStubsSourceStubber::hasClass()can never have been cached. For those names we now skip the key hashing and thestat()too. The wrapped locator is still asked, so class-alias resolution behaves as before. Classes were 88% of the negative lookups; functions and constants have no cheap public check and just go through the guard.Measurements
Measured with an instrumented phar on slevomat (full analysis, result cache deleted in every leg, outputs byte-identical):
Tests
FileCacheStorageTestpins the one hazard the guard introduces: PHP's stat cache must not answer aload()after a miss →save()with the stale negative. I mutated the guard to memoize misses andtestEntrySavedAfterAMissIsLoadedBackfailed; with the real guard it passes.make tests(21416 tests),make phpstanandmake csall pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01C2cdRMUX8Be44eUzEM6RQ2