Skip to content

Commit 06fbac4

Browse files
ondrejmirtesclaude
andcommitted
Replace PhpClassReflectionExtension::evictPrivateSymbols() with a shared 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
1 parent 5a1c8cd commit 06fbac4

2 files changed

Lines changed: 41 additions & 44 deletions

File tree

‎src/Reflection/ClassReflection.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,9 @@ public function evictPrivateSymbols(): void
716716

717717
unset($this->methods[$name]);
718718
}
719-
$this->classReflectionExtensionRegistryProvider->getRegistry()->getPhpClassReflectionExtension()->evictPrivateSymbols($this->getCacheKey());
719+
// PhpClassReflectionExtension's member caches are governed by their own LRU
720+
// (see PhpClassReflectionExtension::touchMemberCacheKey()) instead of per-class
721+
// private-symbol eviction.
720722
}
721723

722724
/** @deprecated Use getInstanceProperty or getStaticProperty */

‎src/Reflection/Php/PhpClassReflectionExtension.php‎

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
use PHPStan\Type\TypehintHelper;
6161
use PHPStan\Type\UnionType;
6262
use function array_key_exists;
63+
use function array_key_first;
6364
use function array_keys;
6465
use function array_map;
6566
use function array_slice;
@@ -73,6 +74,11 @@
7374
final class PhpClassReflectionExtension
7475
{
7576

77+
private const MEMBER_CACHE_KEYS_MAX = 2048;
78+
79+
/** @var array<string, true> shared LRU over the member cache keys below; first entry = least recently used */
80+
private array $memberCacheOrder = [];
81+
7682
/** @var PhpPropertyReflection[][] */
7783
private array $propertiesIncludingAnnotations = [];
7884

@@ -112,52 +118,37 @@ public function __construct(
112118
{
113119
}
114120

115-
public function evictPrivateSymbols(string $classCacheKey): void
121+
/**
122+
* Moves the cache key to the most-recently-used position of the shared LRU governing
123+
* all four member caches; evicts the least recently used key's entries from all of
124+
* them once the limit is reached.
125+
*
126+
* Replaces the former evictPrivateSymbols(): instead of dropping only private members
127+
* of the just-analysed class (public/protected members accumulated for the whole
128+
* process, measured at hundreds of MB on large codebases), classes not used recently
129+
* are evicted wholesale — misses are pure recomputation.
130+
*/
131+
private function touchMemberCacheKey(string $cacheKey): void
116132
{
117-
foreach ($this->propertiesIncludingAnnotations as $key => $properties) {
118-
if ($key !== $classCacheKey) {
119-
continue;
120-
}
121-
foreach ($properties as $name => $property) {
122-
if (!$property->isPrivate()) {
123-
continue;
124-
}
125-
unset($this->propertiesIncludingAnnotations[$key][$name]);
126-
}
127-
}
128-
foreach ($this->nativeProperties as $key => $properties) {
129-
if ($key !== $classCacheKey) {
130-
continue;
131-
}
132-
foreach ($properties as $name => $property) {
133-
if (!$property->isPrivate()) {
134-
continue;
135-
}
136-
unset($this->nativeProperties[$key][$name]);
137-
}
133+
if (isset($this->memberCacheOrder[$cacheKey])) {
134+
unset($this->memberCacheOrder[$cacheKey]);
135+
$this->memberCacheOrder[$cacheKey] = true;
136+
return;
138137
}
139-
foreach ($this->methodsIncludingAnnotations as $key => $methods) {
140-
if ($key !== $classCacheKey) {
141-
continue;
142-
}
143-
foreach ($methods as $name => $method) {
144-
if (!$method->isPrivate()) {
145-
continue;
146-
}
147-
unset($this->methodsIncludingAnnotations[$key][$name]);
148-
}
149-
}
150-
foreach ($this->nativeMethods as $key => $methods) {
151-
if ($key !== $classCacheKey) {
152-
continue;
153-
}
154-
foreach ($methods as $name => $method) {
155-
if (!$method->isPrivate()) {
156-
continue;
157-
}
158-
unset($this->nativeMethods[$key][$name]);
159-
}
138+
139+
$this->memberCacheOrder[$cacheKey] = true;
140+
if (count($this->memberCacheOrder) <= self::MEMBER_CACHE_KEYS_MAX) {
141+
return;
160142
}
143+
144+
$evictKey = array_key_first($this->memberCacheOrder);
145+
unset(
146+
$this->memberCacheOrder[$evictKey],
147+
$this->methodsIncludingAnnotations[$evictKey],
148+
$this->nativeMethods[$evictKey],
149+
$this->propertiesIncludingAnnotations[$evictKey],
150+
$this->nativeProperties[$evictKey],
151+
);
161152
}
162153

163154
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
@@ -171,6 +162,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa
171162
if ($scope->isInClass()) {
172163
$cacheKey = sprintf('%s-%s', $cacheKey, $scope->getClassReflection()->getCacheKey());
173164
}
165+
$this->touchMemberCacheKey($cacheKey);
174166
if (!isset($this->propertiesIncludingAnnotations[$cacheKey][$propertyName])) {
175167
$this->propertiesIncludingAnnotations[$cacheKey][$propertyName] = $this->createProperty($classReflection, $propertyName, $scope, true);
176168
}
@@ -180,6 +172,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa
180172

181173
public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection
182174
{
175+
$this->touchMemberCacheKey($classReflection->getCacheKey());
183176
if (!isset($this->nativeProperties[$classReflection->getCacheKey()][$propertyName])) {
184177
$property = $this->createProperty($classReflection, $propertyName, new OutOfClassScope(), false);
185178
$this->nativeProperties[$classReflection->getCacheKey()][$propertyName] = $property;
@@ -535,6 +528,7 @@ public function hasMethod(ClassReflection $classReflection, string $methodName):
535528

536529
public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection
537530
{
531+
$this->touchMemberCacheKey($classReflection->getCacheKey());
538532
if (isset($this->methodsIncludingAnnotations[$classReflection->getCacheKey()][$methodName])) {
539533
return $this->methodsIncludingAnnotations[$classReflection->getCacheKey()][$methodName];
540534
}
@@ -558,6 +552,7 @@ public function hasNativeMethod(ClassReflection $classReflection, string $method
558552

559553
public function getNativeMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection
560554
{
555+
$this->touchMemberCacheKey($classReflection->getCacheKey());
561556
if (isset($this->nativeMethods[$classReflection->getCacheKey()][$methodName])) {
562557
return $this->nativeMethods[$classReflection->getCacheKey()][$methodName];
563558
}

0 commit comments

Comments
 (0)