Skip to content

Store uses of a file once in the file type cache and in the result cache's exported PHPDoc nodes - #6577

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-566kn9f
Closed

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-566kn9f

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

A file with many use statements and many PHPDoc blocks made the file type cache entry huge, because every name scope stored its own copy of the file's import map. Loading such an entry could use more memory than building it from scratch. This PR stores each import map once per cache entry. The same problem existed in the result cache's exported PHPDoc nodes, and this PR fixes that too.

On a reproducer shaped like the issue's (572 imports, 1150 methods with @return PHPDocs):

before after
file type cache entry 47 MB ~2 MB (cache dir 60 MB → 15 MB)
warm run consumed memory (--debug -vvv) 305 MB 78.5 MB (cold: 184 MB)
resultCache.php 25.7 MB 2.8 MB

Changes

  • src/Analyser/IntermediaryNameScope.php: new dehydrateMap() / hydrateMap(). The dehydrated form lists every distinct array (uses, constUses, template tags, type aliases) and every scope, including parents, once. Scopes refer to them by index. Removed intern(), which only rebuilt the sharing after the full copy was already unserialized.
  • src/Type/FileTypeMapper.php: saves and loads the dehydrated form. The cache key is bumped to v7.
  • src/Dependency/ExportedNode/ExportedPhpDocNode.php: holds an ExportedNameScope instead of separate namespace/uses/constUses fields. The JSON format is unchanged.
  • src/Dependency/ExportedNameScope.php: new equals() and decode(). decode() reuses the previously decoded scope when it is equal, so PHPDoc nodes of one file that a worker sends as JSON share one scope again in the main process.
  • src/Dependency/ExportedNodeResolver.php: passes the shared name scope.
  • src/Analyser/ResultCache/ResultCacheManager.php: CACHE_VERSION is bumped because the serialized shape of ExportedPhpDocNode changed.
  • The other Cache::save() callers (reflection exports in the source locators) don't store per-member copies of file-level arrays, so they need no change.

Root cause

The file cache and the result cache both use serialize(). It keeps objects that are shared within one payload shared, but it writes arrays by value. Two places kept the same file-level arrays in many objects:

  • IntermediaryNameScope::$uses / $constUses (and template/type-alias arrays), one per PHPDoc'd member, in the file type cache.
  • ExportedPhpDocNode::$uses / $constUses, one per exported PHPDoc, in the result cache's exportedNodes.

So both payloads grew with the number of imports times the number of PHPDocs. The fix stores each array once: by index in the file type cache, and through a shared object in the result cache.

Test

  • FileTypeMapperTest::testNameScopeMapIsStoredInCacheWithSharedValues uses a cache storage that serializes. It checks that the stored entry for data/bug-15304.php contains each imported name once (it was 7 before). It also checks that a second FileTypeMapper reading that entry resolves aliased @return types and class/method template types correctly.
  • ExportedNodeResolverTest::testUsesAreStoredOncePerFile checks that the serialized exported nodes of the same file contain each import once (it was 6 before). It also runs a JSON encode/decode round trip, as the parallel worker path does, and checks the decoded nodes are equal and still share their name scope.

Fixes phpstan/phpstan#15304

…che's exported PHPDoc nodes

- serialize() keeps shared objects shared but writes every array by value, so each
  IntermediaryNameScope in a cached name scope map carried its own copy of the file's
  uses/constUses. The cache entry and the peak memory of loading it grew with the number
  of imports times the number of PHPDocs, so a warm run could use more memory than a cold one.
- Add IntermediaryNameScope::dehydrateMap()/hydrateMap(): FileTypeMapper now stores every
  distinct array (uses, constUses, template tags, type aliases) and every scope once and
  refers to them by index. This replaces the interning after load
  (IntermediaryNameScope::intern()), because the peak is already reached while unserializing.
  File type cache key bumped to v7.
- Analogous case in the result cache: every ExportedPhpDocNode held its own uses/constUses
  arrays, so exportedNodes grew the same way. ExportedPhpDocNode now holds the
  ExportedNameScope shared by all PHPDocs of a file, which serialize() writes once per file.
  ExportedNameScope::decode() reuses the previously decoded equal scope, so nodes sent by
  workers as JSON share it again in the main process. Result cache version bumped.
- Other Cache::save() callers (reflection exports in the source locators) don't store
  per-member copies of file-level arrays, so they don't have this problem.
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

3 participants