Skip to content

LRU-cap resolved local type aliases in UsefulTypeAliasResolver#5989

Closed
ondrejmirtes wants to merge 2 commits into
2.2.xfrom
type-alias-resolver-lru
Closed

LRU-cap resolved local type aliases in UsefulTypeAliasResolver#5989
ondrejmirtes wants to merge 2 commits into
2.2.xfrom
type-alias-resolver-lru

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Jul 3, 2026

Copy link
Copy Markdown
Member

Problem

UsefulTypeAliasResolver::$resolvedLocalTypeAliases caches every resolved @phpstan-type local alias forever, keyed by Class::alias. Resolved alias Types transitively pin ClassReflections (ObjectType::$classReflection, EnumCaseObjectType::$classReflection, …) and through them whole BetterReflection trees.

Retention-path instrumentation on the ShipMonk backend named this cache as the single largest type-level pin of the reflection universe — a sampled path:

ReflectionNamedType ← ReflectionEnum ← ClassReflection
  ← EnumCaseObjectType::$classReflection ← UnionType
  ← UsefulTypeAliasResolver::$resolvedLocalTypeAliases[]

Types are stronger pins than reflections themselves: bounding this one cache freed more memory than bounding any reflection-side cache in the same experiment series.

Change

Cap $resolvedLocalTypeAliases at 2048 entries with LRU eviction (move-to-end on hit, evict-first on insert). Evicted aliases are re-resolved on demand — pure recomputation. Global aliases are untouched (bounded by config).

Measured (2,358-file project, single process)

The 2048 cap could be promoted to a cache.* parameter if desired.

Tests: LocalTypeAliasesRuleTest + type-alias NSRT assertions pass; phpcs clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NrTvR9j1mW2NNNC8RkuTsF

Configurability + default (added)

The cap is now a cache.* parameter (cache.resolvedLocalTypeAliasesCountMax, default 2048, 0 disables eviction), following the phpStormStubsNodesCountMax pattern.

The default comes from a cap sweep on the 2,358-file project (all LRU caps set to the sweep value, single process, error output identical — 776 — 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 fresh object graphs while the old copies are still pinned by sibling caches, so duplicates accumulate (this also explains why very small caps regressed memory on large projects before). Too-large caps converge to unbounded retention. Elapsed time shows no trend across the whole range — the knob trades memory only, so 2048 is chosen purely on the memory optimum.

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
@ondrejmirtes ondrejmirtes force-pushed the type-alias-resolver-lru branch from b4348df to e674a79 Compare July 3, 2026 19:58
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
@ondrejmirtes

Copy link
Copy Markdown
Member Author

Combined into #5988 per review preference.

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