Skip to content

Commit 945ba95

Browse files
ondrejmirtesclaude
andcommitted
Share one NamespaceUses between the name scopes of a file
Every IntermediaryNameScope held its own copy of the file's uses and constUses. serialize() writes arrays by value, so the name scope map FileTypeMapper stores in the file cache grew with the number of imports times the number of classes, functions and methods - and loading it could take more memory than building it from scratch. IntermediaryNameScope now refers to a NamespaceUses, and FileTypeMapper tracks the namespace and the use statements with NamespaceUsesTracker, which hands out one instance until the next namespace or use statement. serialize() keeps shared objects shared, so an entry stores the imports once per namespace and use-statement state. intern(), which restored the sharing only after the whole entry had been unserialized, and __set_state(), left over from the var_export()ed file cache, are no longer needed. Reproducer from the issue (572 imports, 1150 documented methods): largest file cache entry 27.7 MB -> 0.86 MB, consumed memory of a warm run 136 MB -> 8 MB (cold 36 MB -> 12 MB). The name scope map entries of src/Analyser src/Rules src/Type take 22.2 MB instead of 42.0 MB, peak RSS of that analysis is unchanged (579 MB). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182BrC263VCQTibEPTJX2UY
1 parent e52951a commit 945ba95

6 files changed

Lines changed: 202 additions & 141 deletions

File tree

‎src/Analyser/IntermediaryNameScope.php‎

Lines changed: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,23 @@
33
namespace PHPStan\Analyser;
44

55
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
6-
use function serialize;
76

87
final class IntermediaryNameScope
98
{
109

1110
/**
12-
* @api
13-
* @param non-empty-string|null $namespace
14-
* @param array<string, string> $uses alias(string) => fullName(string)
1511
* @param array<string, array{string, TemplateTagValueNode}> $templatePhpDocNodes
16-
* @param array<string, string> $constUses alias(string) => fullName(string)
1712
* @param array<string, true> $typeAliasesMap
1813
* @param array{string, string, string, string|null, string|null}|null $traitData
1914
*/
2015
public function __construct(
21-
private ?string $namespace,
22-
private array $uses,
16+
private NamespaceUses $namespaceUses,
2317
private ?string $className = null,
2418
private ?string $functionName = null,
2519
private array $templatePhpDocNodes = [],
2620
private ?self $parent = null,
2721
private array $typeAliasesMap = [],
2822
private bool $bypassTypeAliases = false,
29-
private array $constUses = [],
3023
private ?string $typeAliasClassName = null,
3124
private ?array $traitData = null,
3225
)
@@ -38,23 +31,23 @@ public function __construct(
3831
*/
3932
public function getNamespace(): ?string
4033
{
41-
return $this->namespace;
34+
return $this->namespaceUses->getNamespace();
4235
}
4336

4437
/**
4538
* @return array<string, string>
4639
*/
4740
public function getUses(): array
4841
{
49-
return $this->uses;
42+
return $this->namespaceUses->getUses();
5043
}
5144

5245
/**
5346
* @return array<string, string>
5447
*/
5548
public function getConstUses(): array
5649
{
57-
return $this->constUses;
50+
return $this->namespaceUses->getConstUses();
5851
}
5952

6053
public function getClassName(): ?string
@@ -78,15 +71,13 @@ public function getTemplatePhpDocNodes(): array
7871
public function withTraitData(string $fileName, string $className, string $traitName, ?string $lookForTraitName, ?string $docComment): self
7972
{
8073
return new self(
81-
$this->namespace,
82-
$this->uses,
74+
$this->namespaceUses,
8375
$this->className,
8476
$this->functionName,
8577
$this->templatePhpDocNodes,
8678
$this->parent,
8779
$this->typeAliasesMap,
8880
$this->bypassTypeAliases,
89-
$this->constUses,
9081
$this->typeAliasClassName,
9182
[$fileName, $className, $traitName, $lookForTraitName, $docComment],
9283
);
@@ -102,68 +93,18 @@ public function unsetTemplatePhpDocNodes(array $namesToUnset): self
10293
unset($templatePhpDocNodes[$name]);
10394
}
10495
return new self(
105-
$this->namespace,
106-
$this->uses,
96+
$this->namespaceUses,
10797
$this->className,
10898
$this->functionName,
10999
$templatePhpDocNodes,
110100
$this->parent,
111101
$this->typeAliasesMap,
112102
$this->bypassTypeAliases,
113-
$this->constUses,
114103
$this->typeAliasClassName,
115104
$this->traitData,
116105
);
117106
}
118107

119-
/**
120-
* Restores sharing of identical property values and parent scopes after hydration from the file cache.
121-
*
122-
* serialize() keeps objects shared within an entry but stores arrays by value, so
123-
* every hydrated scope carries its own copy of the same uses maps.
124-
* Without interning, the name scope map of a file with many members takes up
125-
* many times more memory when loaded from the cache than when freshly created.
126-
*
127-
* @param array<string, self|array<mixed>> $pool
128-
*/
129-
public function intern(array &$pool): self
130-
{
131-
$key = serialize($this);
132-
if (isset($pool[$key])) {
133-
/** @var self */
134-
return $pool[$key];
135-
}
136-
137-
$this->uses = self::internArray($pool, $this->uses);
138-
$this->templatePhpDocNodes = self::internArray($pool, $this->templatePhpDocNodes);
139-
$this->typeAliasesMap = self::internArray($pool, $this->typeAliasesMap);
140-
$this->constUses = self::internArray($pool, $this->constUses);
141-
if ($this->parent !== null) {
142-
$this->parent = $this->parent->intern($pool);
143-
}
144-
145-
return $pool[$key] = $this;
146-
}
147-
148-
/**
149-
* @template T of array<mixed>
150-
* @param array<string, self|array<mixed>> $pool
151-
* @param T $value
152-
* @return T
153-
*/
154-
private static function internArray(array &$pool, array $value): array
155-
{
156-
$key = 'a:' . serialize($value);
157-
if (isset($pool[$key])) {
158-
/** @var T */
159-
return $pool[$key];
160-
}
161-
162-
$pool[$key] = $value;
163-
164-
return $value;
165-
}
166-
167108
/**
168109
* @return array{string, string, string, string|null, string|null}|null
169110
*/
@@ -195,24 +136,4 @@ public function getClassNameForTypeAlias(): ?string
195136
return $this->typeAliasClassName;
196137
}
197138

198-
/**
199-
* @param array<string, mixed> $properties
200-
*/
201-
public static function __set_state(array $properties): self
202-
{
203-
return new self(
204-
$properties['namespace'],
205-
$properties['uses'],
206-
$properties['className'],
207-
$properties['functionName'],
208-
$properties['templatePhpDocNodes'],
209-
$properties['parent'],
210-
$properties['typeAliasesMap'],
211-
$properties['bypassTypeAliases'],
212-
$properties['constUses'],
213-
$properties['typeAliasClassName'],
214-
$properties['traitData'],
215-
);
216-
}
217-
218139
}

‎src/Analyser/NamespaceUses.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
/**
66
* The namespace and the use statements in effect at a point in a file.
77
*
8-
* This is everything an ExportedPhpDocNode records about the scope a PHPDoc was written in, and
8+
* NamespaceUsesTracker hands out one instance until the next namespace or use statement, so
9+
* everything created under the same imports shares it. serialize() keeps shared objects shared
10+
* but writes arrays by value - the name scope map FileTypeMapper stores in the file cache
11+
* therefore holds the imports of a file once instead of once per class, function and method.
12+
*
13+
* It is also everything an ExportedPhpDocNode records about the scope a PHPDoc was written in, and
914
* all of it is read straight off the AST by ExportedNodeVisitor. Asking FileTypeMapper for a
1015
* NameScope instead would build the name scope map of the whole file, which resolves every PHPDoc
1116
* in it - far more than the exported nodes need, and it happens in the main process during a

‎src/Analyser/NamespaceUsesTracker.php‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
/**
1010
* Keeps the namespace and the use statements seen so far while walking a file's statements.
1111
*
12-
* Both places that build exported nodes drive one of these - ExportedNodeVisitor for a result
13-
* cache restore, DependencyResolver during the analysis - so that the PHPDoc scope recorded in
14-
* the cache and the one computed when checking the cache are produced by the same code.
12+
* FileTypeMapper drives one while building a name scope map, and so do both places that build
13+
* exported nodes - ExportedNodeVisitor for a result cache restore, DependencyResolver during the
14+
* analysis - so that the PHPDoc scope recorded in the result cache and the one computed when
15+
* checking the cache are produced by the same code as the one PHPDocs are resolved in.
1516
*
16-
* It mirrors what FileTypeMapper does while building a name scope map, except that the uses are
17-
* cleared when a namespace is entered rather than when it is left. Namespace blocks cover every
18-
* statement of a file, so nothing can observe the difference.
17+
* The uses are cleared when a namespace is entered rather than when it is left. Namespace blocks
18+
* cover every statement of a file, so nothing can observe the difference.
1919
*/
2020
final class NamespaceUsesTracker
2121
{

‎src/Type/FileTypeMapper.php‎

Lines changed: 14 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpParser\Node;
88
use PHPStan\Analyser\IntermediaryNameScope;
99
use PHPStan\Analyser\NameScope;
10+
use PHPStan\Analyser\NamespaceUsesTracker;
1011
use PHPStan\BetterReflection\Util\GetLastDocComment;
1112
use PHPStan\Broker\AnonymousClassNameHelper;
1213
use PHPStan\Cache\Cache;
@@ -50,7 +51,6 @@
5051
use function sprintf;
5152
use function str_contains;
5253
use function str_starts_with;
53-
use function strtolower;
5454

5555
#[AutowiredService]
5656
final class FileTypeMapper
@@ -369,7 +369,9 @@ private function getNameScopeMap(string $fileName): array
369369
// (https://github.com/phpstan/phpstan/issues/15037) - a parse that
370370
// silently dropped every comment cached PHPDoc-less name-scope maps,
371371
// and the content hashes cannot tell them apart from real ones.
372-
$variableCacheKey = sprintf('v6-%s', ComposerHelper::getPhpDocParserVersion());
372+
// v7: IntermediaryNameScope refers to a NamespaceUses shared with the other name scopes
373+
// created under the same use statements
374+
$variableCacheKey = sprintf('v7-%s', ComposerHelper::getPhpDocParserVersion());
373375
$cached = $this->loadCachedPhpDocNodeMap($cacheKey, $variableCacheKey);
374376
if ($cached === null) {
375377
[$nameScopeMap, $files] = $this->createPhpDocNodeMap($fileName, null, null, [], $fileName);
@@ -415,11 +417,6 @@ private function loadCachedPhpDocNodeMap(string $cacheKey, string $variableCache
415417
}
416418

417419
if ($useCache) {
418-
$pool = [];
419-
foreach ($nameScopeMap as $nameScopeKey => $intermediaryNameScope) {
420-
$nameScopeMap[$nameScopeKey] = $intermediaryNameScope->intern($pool);
421-
}
422-
423420
return [$nameScopeMap, array_keys($filesWithHashes)];
424421
}
425422
}
@@ -449,19 +446,18 @@ private function createPhpDocNodeMap(string $fileName, ?string $lookForTrait, ?s
449446
$classStack[] = $traitUseClass;
450447
$typeAliasStack[] = [];
451448
}
452-
$namespace = null;
449+
$namespaceUsesTracker = new NamespaceUsesTracker();
453450

454451
$traitFound = false;
455452

456453
$files = [$fileName];
457454

458455
/** @var array<string|null> $functionStack */
459456
$functionStack = [];
460-
$uses = [];
461-
$constUses = [];
462457
$this->processNodes(
463458
$this->phpParser->parseFile($fileName),
464-
function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, $activeTraitResolutions, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, &$namespace, &$functionStack, &$uses, &$constUses, &$files): ?int {
459+
function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, $activeTraitResolutions, &$nameScopeMap, &$typeMapStack, &$typeAliasStack, &$classStack, $namespaceUsesTracker, &$functionStack, &$files): ?int {
460+
$namespaceUsesTracker->enterNode($node);
465461
if ($node instanceof Node\Stmt\ClassLike) {
466462
if ($traitFound && $fileName === $originalClassFileName) {
467463
return self::SKIP_NODE;
@@ -490,7 +486,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
490486
if ($traitFound) {
491487
return self::SKIP_NODE;
492488
}
493-
$className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
489+
$className = ltrim(sprintf('%s\\%s', $namespaceUsesTracker->getNamespaceUses()->getNamespace(), $node->name->name), '\\');
494490
}
495491
$classStack[] = $className;
496492
$functionStack[] = null;
@@ -502,7 +498,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
502498
$functionStack[] = $node->name->name;
503499
}
504500
} elseif ($node instanceof Node\Stmt\Function_) {
505-
$functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
501+
$functionStack[] = ltrim(sprintf('%s\\%s', $namespaceUsesTracker->getNamespaceUses()->getNamespace(), $node->name->name), '\\');
506502
} elseif ($node instanceof Node\PropertyHook) {
507503
$propertyName = $node->getAttribute('propertyName');
508504
if ($propertyName !== null) {
@@ -535,29 +531,25 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
535531
$parentNameScope = array_last($typeMapStack);
536532

537533
$typeMapStack[] = new IntermediaryNameScope(
538-
$namespace,
539-
$uses,
534+
$namespaceUsesTracker->getNamespaceUses(),
540535
$className,
541536
$functionName,
542537
$this->chooseTemplateTagValueNodesByPriority($phpDocNode->getTags()),
543538
$parentNameScope,
544539
array_last($typeAliasStack) ?? [],
545-
constUses: $constUses,
546540
typeAliasClassName: $lookForTrait,
547541
);
548542
} elseif ($node instanceof Node\Stmt\ClassLike) {
549543
$typeAliasStack[] = [];
550544
} else {
551545
$parentNameScope = array_last($typeMapStack);
552546
$typeMapStack[] = new IntermediaryNameScope(
553-
$namespace,
554-
$uses,
547+
$namespaceUsesTracker->getNamespaceUses(),
555548
$className,
556549
$functionName,
557550
[],
558551
$parentNameScope,
559552
array_last($typeAliasStack) ?? [],
560-
constUses: $constUses,
561553
typeAliasClassName: $lookForTrait,
562554
);
563555
}
@@ -582,14 +574,12 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
582574
$parentNameScope = array_last($typeMapStack);
583575
$typeAliasesMap = array_last($typeAliasStack) ?? [];
584576
$nameScopeMap[$nameScopeKey] = new IntermediaryNameScope(
585-
$namespace,
586-
$uses,
577+
$namespaceUsesTracker->getNamespaceUses(),
587578
$className,
588579
$functionName,
589580
$parentNameScope !== null ? $parentNameScope->getTemplatePhpDocNodes() : [],
590581
$parentNameScope !== null ? $parentNameScope->getParent() : null,
591582
$typeAliasesMap,
592-
constUses: $constUses,
593583
typeAliasClassName: $lookForTrait,
594584
);
595585
}
@@ -602,28 +592,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
602592
return null;
603593
}
604594

605-
if ($node instanceof Node\Stmt\Namespace_) {
606-
$namespace = $node->name !== null ? (string) $node->name : null;
607-
} elseif ($node instanceof Node\Stmt\Use_) {
608-
if ($node->type === Node\Stmt\Use_::TYPE_NORMAL) {
609-
foreach ($node->uses as $use) {
610-
$uses[strtolower($use->getAlias()->name)] = (string) $use->name;
611-
}
612-
} elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
613-
foreach ($node->uses as $use) {
614-
$constUses[strtolower($use->getAlias()->name)] = (string) $use->name;
615-
}
616-
}
617-
} elseif ($node instanceof Node\Stmt\GroupUse) {
618-
$prefix = (string) $node->prefix;
619-
foreach ($node->uses as $use) {
620-
if ($node->type === Node\Stmt\Use_::TYPE_NORMAL || $use->type === Node\Stmt\Use_::TYPE_NORMAL) {
621-
$uses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
622-
} elseif ($node->type === Node\Stmt\Use_::TYPE_CONSTANT || $use->type === Node\Stmt\Use_::TYPE_CONSTANT) {
623-
$constUses[strtolower($use->getAlias()->name)] = sprintf('%s\\%s', $prefix, (string) $use->name);
624-
}
625-
}
626-
} elseif ($node instanceof Node\Stmt\TraitUse) {
595+
if ($node instanceof Node\Stmt\TraitUse) {
627596
$traitMethodAliases = [];
628597
foreach ($node->adaptations as $traitUseAdaptation) {
629598
if (!$traitUseAdaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
@@ -694,7 +663,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
694663

695664
return null;
696665
},
697-
static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack, &$constUses): void {
666+
static function (Node $node, $callbackResult) use (&$functionStack, &$classStack, &$typeAliasStack, &$typeMapStack): void {
698667
if ($node instanceof Node\Stmt\ClassLike) {
699668
if (count($classStack) === 0) {
700669
throw new ShouldNotHappenException();
@@ -712,10 +681,6 @@ static function (Node $node, $callbackResult) use (&$namespace, &$functionStack,
712681
}
713682

714683
array_pop($functionStack);
715-
} elseif ($node instanceof Node\Stmt\Namespace_) {
716-
$namespace = null;
717-
$uses = [];
718-
$constUses = [];
719684
} elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
720685
if (count($functionStack) === 0) {
721686
throw new ShouldNotHappenException();

0 commit comments

Comments
 (0)