LRU-cap the per-class reflection registries and resolved local type aliases#5988
Open
ondrejmirtes wants to merge 6 commits into
Open
LRU-cap the per-class reflection registries and resolved local type aliases#5988ondrejmirtes wants to merge 6 commits into
ondrejmirtes wants to merge 6 commits into
Conversation
MemoizingReflectionProvider::$classes, BetterReflectionProvider::$classReflections and MemoizingReflector::$classReflections/$functionReflections grow with every distinct class/function touched during the run (9k+ classes per worker on a large project) and are the canonical pins of the reflection object graphs: bounding downstream caches (member reflections, dependency lists) frees little while these registries still hold every ClassReflection. Cap each at 2048 entries with least-recently-used eviction (move-to-end on hit, evict-first on insert). Negative (null) lookup entries participate in the cap as well, so repeated misses cannot grow the maps unboundedly either. Evicted entries are recomputed on the next request — for the providers this means a fresh ClassReflection instance for the same class may be created after eviction; PHPStan compares class reflections by name, not identity, and error output on a 2,358-file project is byte-identical with eviction active. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF
Default 2048, 0 disables eviction. The default comes from a cap sweep on a 2,358-file project (all LRU caps set to the sweep value, single process, error output identical at every point): cap elapsed end usage 512 101 s 936.5 MB 1024 93 s 912.5 MB 2048 88 s 880.5 MB 4096 84 s 894.5 MB 8192 92 s 930.6 MB unlimited 98 s 930.5 MB The curve is U-shaped: too-small caps are actively harmful (evicted entries are recreated as new object graphs while the old ones are still pinned by sibling caches, so duplicates accumulate), too-large caps converge to unbounded retention. Elapsed time shows no trend across the whole range — the knob trades memory only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF
Every resolved @phpstan-type local alias is cached forever, keyed by Class::alias. Resolved alias Types transitively pin ClassReflections (ObjectType::$classReflection, EnumCaseObjectType::$classReflection, ...) and through them whole BetterReflection trees, so on alias-heavy codebases this cache quietly retains a large share of the reflection universe. Retention-path instrumentation on a large project identified it as the single largest type-level pin: bounding it freed 34 MB end-of-run memory at 2,358 analysed files — more than any reflection-side cache — with byte-identical error output and no measurable time cost (interleaved A/B, 100 s vs 95 s). Cap the cache at 2048 entries with least-recently-used eviction; evicted aliases are re-resolved on demand. Global aliases are unaffected (bounded by config). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF
76200ea to
bf2f8f9
Compare
Default 2048, 0 disables eviction. Chosen from a cap sweep on a 2,358-file project (all LRU caps set to the sweep value, error output identical at every point): memory is U-shaped in the cap — 936.5 MB at 512, 880.5 MB at 2048, 930.5 MB uncapped — because too-small caps recreate evicted object graphs as duplicates while the old ones are still pinned by sibling caches, and too-large caps converge to unbounded retention. Elapsed time shows no trend across the whole range (84-101 s noise band). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF
bf2f8f9 to
196c89b
Compare
…t stubBetterReflectionProvider definition The derived container defines the provider as an explicit neon service, which does not go through the attribute-based autowiring that supplies the parameter to the main container's service. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF
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.
Combines the two LRU PRs into one (per review preference; #5989 is closed in favour of this):
1. LRU-cap the per-class reflection registries
MemoizingReflectionProvider::$classes,BetterReflectionProvider::$classReflectionsandMemoizingReflector::$classReflections/$functionReflectionsgrow with every distinct class/function touched (9k+ classes per worker on a large project) and are the canonical pins of the reflection object graphs — bounding downstream caches (e.g. #5986) frees little while these registries still hold everyClassReflection. Negative (null) lookup entries participate in the caps too. Configurable viacache.reflectionRegistryCountMax(default 2048,0disables).2. LRU-cap resolved local type aliases
UsefulTypeAliasResolver::$resolvedLocalTypeAliasescaches every resolved@phpstan-typelocal alias forever; resolved alias Types transitively pinClassReflections and whole BetterReflection trees — measured as the single largest type-level pin. Configurable viacache.resolvedLocalTypeAliasesCountMax(default 2048,0disables).Defaults from a cap sweep
2,358-file project, all LRU caps set to the sweep value, single process, error output identical (776) at every point:
The curve is U-shaped: too-small caps are actively harmful (evicted entries are recreated as fresh object graphs while old copies are still pinned by sibling caches — duplicates accumulate), too-large caps converge to unbounded retention. Elapsed shows no trend across the range — the knob trades memory only.
Evicted entries are recomputed on demand; a fresh
ClassReflectioninstance for the same class can exist after eviction — PHPStan compares class reflections by name, not identity, and error output is byte-identical with eviction active (89/776 errors, two configs).PHPStanTestCasepasses0(unlimited) to keep test behavior bit-exact.Full prototype series on the measured project (with #5986): −76 MB end usage / −74 MB peak (−8%) single-process.
Tests:
ClassReflectionTest,LocalTypeAliasesRuleTest,CallMethodsRuleTestpass; phpcs clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF