Skip to content

Share one NamespaceUses between the name scopes of a file - #6583

Merged
ondrejmirtes merged 2 commits into
2.3.xfrom
intermediary-name-scope-shared-uses
Sep 25, 2026
Merged

ondrejmirtes merged 2 commits into
2.3.xfrom
intermediary-name-scope-shared-uses

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Every IntermediaryNameScope held its own copy of the file's uses and constUses. serialize() keeps shared objects shared but 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. Loading such an entry could take more memory than building it from scratch.

Two commits:

  1. Move ExportedNameScope to PHPStan\Analyser\NamespaceUses (with its tracker, now NamespaceUsesTracker). This is a pure rename. The tracker already hands out one instance until the next namespace/use statement, and it duplicated what FileTypeMapper did by hand.
  2. Share one NamespaceUses between the name scopes of a file. IntermediaryNameScope now refers to a NamespaceUses instead of carrying the arrays, and FileTypeMapper drives a NamespaceUsesTracker in place of its own namespace/use handling. A cache entry stores the imports once per namespace and use-statement state.
    • intern() is removed. It restored the sharing only after the whole entry had been unserialized.
    • __set_state() is removed. It was a leftover from the var_export()ed file cache.
    • The file cache key is bumped to v7.

The IntermediaryNameScope constructor loses its @api tag and changes signature. A GitHub code search finds it constructed only inside committed PHPStan cache directories, never in extensions.

Numbers

Reproducer from the issue (572 imports, 1150 documented methods):

before after
largest file cache entry 27.7 MB 0.86 MB
consumed memory, warm run 136 MB 8 MB
consumed memory, cold run 36 MB 12 MB

On src/Analyser src/Rules src/Type (single process, warm file cache):

  • The name scope map entries take 22.2 MB instead of 42.0 MB.
  • Peak RSS is unchanged at 579 MB, so dropping intern() does not bring back the memory it was introduced to save.
  • User CPU was 55.7–58.0 s against 59.3–59.8 s on 2.3.x (one ABBA round).

Test

FileTypeMapperTest::testNameScopeMapStoresImportsOnce uses a cache storage that serializes. It fails on 2.3.x: 10 copies of an import where 2 are expected. It also checks the names resolved from the stored entry, including a use statement between two classes and a second namespace block.

Supersedes #6577.

Closes phpstan/phpstan#15304

🤖 Generated with Claude Code

https://claude.ai/code/session_0182BrC263VCQTibEPTJX2UY

ondrejmirtes and others added 2 commits September 25, 2026 06:16
The namespace and the use statements in effect at a point in a file are
not specific to exported nodes. FileTypeMapper tracks the same data while
building a name scope map, so move the value object and its tracker next
to the name scopes where both can use them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182BrC263VCQTibEPTJX2UY
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
@ondrejmirtes
ondrejmirtes merged commit 945ba95 into 2.3.x Sep 25, 2026
521 of 525 checks passed
@ondrejmirtes
ondrejmirtes deleted the intermediary-name-scope-shared-uses branch September 25, 2026 04:29
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.

File type cache stores the full import map for every PHPDoc, warm run uses more memory than cold run

1 participant