Replace PhpClassReflectionExtension::evictPrivateSymbols() with a shared LRU over the member caches#5986
Merged
ondrejmirtes merged 2 commits intoJul 3, 2026
Conversation
…red LRU over the member caches The four member caches (methodsIncludingAnnotations, nativeMethods, propertiesIncludingAnnotations, nativeProperties) were unbounded except for private members of the just-analysed class. Public/protected member reflections accumulated for the whole process — measured as the single largest analysis-growth bucket on a large project (334 MB of a 675 MB per-process growth at 2,358 files). The private-only eviction also silently missed the composite `class-scope` cache keys used by getProperty(), because it compared keys with strict equality against the plain class cache key. Replace it with a single insertion-ordered LRU over the first-level cache keys, shared by all four maps: every cache access moves the key to the most-recently-used position; inserting beyond 2048 keys evicts the least recently used key's entries from all four maps at once. Misses are pure recomputation, so eviction cannot change results — verified byte-identical error output on a 2,358-file project, with no measurable time cost (LRU bookkeeping is one unset+set per access, and the previous eviction's full map scan per analysed class is gone). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF
Replaces the MEMBER_CACHE_KEYS_MAX constant with a parameter alongside the other cache limits; 0 means unlimited, same as cache.phpStormStubsNodesCountMax. The 2048 default is confirmed by a sweep on a 2,358-file project (8 workers, result cache cleared, error output byte-identical across all runs): unlimited costs 9.02 GB summed worker peaks, any limit in 512-2048 lands on the same 8.8 GB plateau, and 128-256 give the savings back through evict-recompute churn. No limit-dependent time cost: wall and user CPU varied together across limits (machine scheduling noise), never with the limit. Self-analysis is flat in both time and memory at every limit down to 64. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8cNW3DZDEJjeBK2WXqgP7
This was referenced Jul 3, 2026
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.
Problem
PhpClassReflectionExtension's four member caches (methodsIncludingAnnotations,nativeMethods,propertiesIncludingAnnotations,nativeProperties) are unbounded except forevictPrivateSymbols(), which drops only private members of the just-analysed class. Public/protected member reflections accumulate for the whole process.Instrumented on a large project (ShipMonk backend), these caches were the single largest analysis-growth bucket: 334 MB of a 675 MB per-process growth at 2,358 analysed files (3,710 + 6,258 + 2,501 + 2,073 first-level keys; each entry a full
PhpMethodReflection/PhpPropertyReflectiongraph).The private-only eviction also silently misses the composite
class-scopecache keys used bygetProperty()— it compares keys with!==against the plain class cache key, so scoped private property entries were never evicted.Change
One insertion-ordered LRU over the first-level cache keys, shared by all four maps:
evictPrivateSymbols()(and its per-class full-map scan after every analysed class, plus the delegation fromClassReflection::evictPrivateSymbols()) is removed.Misses are pure recomputation, so eviction cannot change results.
Measured (2,358-file project, single process)
unset+set per access, and the old eviction's full map scan per analysed class is gone)ClassReflections (MemoizingReflectionProvider,DependencyResolver::$classDependencies, rule state), so the standalone win is bounded. The value is replacing an incomplete, quadratic-ish eviction with a general bounded cache — a prerequisite for the eviction to cascade once the sibling holders are bounded too.The 2048 cap could be promoted to a
cache.*parameter likenameScopeMapMemoryCacheCountMaxif desired.Tests:
ClassReflectionTest,TypesAssignedToPropertiesRuleTest,CallMethodsRuleTestpass; phpcs clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF