Skip to content

Keep the restored result cache out of forked workers' memory - #6686

Open
janedbal wants to merge 1 commit into
phpstan:2.3.xfrom
janedbal:restore-drop-stale-reanalysed-entries
Open

janedbal wants to merge 1 commit into
phpstan:2.3.xfrom
janedbal:restore-drop-stale-reanalysed-entries

Conversation

@janedbal

@janedbal janedbal commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Fixes phpstan/phpstan#15387

An incremental run that re-analyses a large part of the project uses about 5x the memory of a run without a result cache. The cause is the forked workers: they inherit the memory that restore() left in the main process.

  • ResultCacheManager::restore() now drops the cached errors, locally ignored errors, lines to ignore, unmatched line ignores, collected data and exported nodes of the files in $filesToAnalyse. Nothing reads them: every merge*() method in process() replaces or unsets the entries of these files. The skip-rewrite check compares the sections only when getFilesToAnalyse() === [], so it does not change.
  • ForkedProcess::start() calls gc_mem_caches() before pcntl_fork(). Without it, each worker allocates into the memory that the parent freed and gets a private copy of it.

The two changes need each other. Dropping the entries alone frees more memory that the workers then copy. gc_mem_caches() alone cannot release the allocator chunks that the stale entries keep in use.

Peak sum of Pss over the main process and the 20 forked workers, with the reproducer from the issue (4001 files, signature of their common parent class changed after a cold run):

Cold run Incremental run, 4001 files
2.3.0 2.0 GB 10.2 - 10.4 GB
this PR 1.9 GB 2.2 - 2.3 GB

The same measurement on 2.2.14 shows that each change alone does not help:

2.2.14 + Incremental run
nothing 7.6 - 8.4 GB
unset only 13.0 GB
gc_mem_caches() only 3.8 GB
both 2.1 - 2.2 GB

The result-cache e2e scenarios pass locally (52 of 54; the two others need a .git directory and a CI environment variable, and fail without this change too).

— Claude

Co-Authored-By: Claude Code

A restored result cache keeps the errors, collected data and other
per-file entries of the files that are going to be re-analysed. Nothing
reads them: the merge in process() replaces them with the fresh results.
restore() now drops them.

Memory that the parent process frees stays in the Zend allocator. A
pcntl_fork()ed worker shares it copy-on-write, allocates into it and so
gets a private copy of it. Each worker does this, so the waste grows with
the number of workers. ForkedProcess now calls gc_mem_caches() before the
fork to give that memory back to the OS.

The two changes need each other. Dropping the entries alone frees more
memory that the workers then copy. gc_mem_caches() alone cannot release
the allocator chunks that the stale entries keep in use.

Whole process tree PSS, 4001 files that extend one class, 20 forked
workers, class signature changed after a cold run:

                          cold      re-analyse all
  2.3.0                   2.0 GB    10.2 - 10.4 GB
  2.3.0 + this change     1.9 GB    2.2 - 2.3 GB

  2.2.14                  2.1 GB    7.6 - 8.4 GB
  2.2.14 + drop only      2.1 GB    13.0 GB
  2.2.14 + gc only        2.1 GB    3.8 GB

Fixes phpstan/phpstan#15387

Co-Authored-By: Claude Code
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.

Incremental run with many files to re-analyse uses ~5x the memory of a full run (forked workers copy the restored result cache)

1 participant