Skip to content

Answer reflection cache misses without a failing include() - #6430

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
cache-miss-fast-path
Sep 12, 2026
Merged

Answer reflection cache misses without a failing include()#6430
ondrejmirtes merged 1 commit into
2.2.xfrom
cache-miss-fast-path

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

FileCacheStorage::load() found out that an entry was never written by letting @include fail. That costs about 29 µs per miss: PHP walks the include_path and builds two warnings, and then the @ throws them away. A stat() 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. CachedPhpInternalSourceLocator sits 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 in FileCacheStorage::load(). This is general and helps every miss path.
  • Stubs-map gate in CachedPhpInternalSourceLocator. The stubber is the only source the wrapped locator reads from, so a class not listed in PhpStormStubsSourceStubber::hasClass() can never have been cached. For those names we now skip the key hashing and the stat() 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):

  • With the guard alone, the cache-miss path dropped from 5.04 s to 1.95 s of fleet-wide time.
  • The whole run dropped by 2.3 s of user CPU, consistent across 4 ABBA pairs.
  • The stubs-map gate removes most of the remaining 1.95 s. It wasn't benchmarked separately.

Tests

  • New FileCacheStorageTest pins the one hazard the guard introduces: PHP's stat cache must not answer a load() after a miss → save() with the stale negative. I mutated the guard to memoize misses and testEntrySavedAfterAMissIsLoadedBack failed; with the real guard it passes.
  • make tests (21416 tests), make phpstan and make cs all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C2cdRMUX8Be44eUzEM6RQ2

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
@ondrejmirtes
ondrejmirtes merged commit 395468a into 2.2.x Sep 12, 2026
499 of 501 checks passed
@ondrejmirtes
ondrejmirtes deleted the cache-miss-fast-path branch September 12, 2026 19:21
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