Skip to content

Skip result cache re-save when nothing changed#5843

Closed
SanderMuller wants to merge 1 commit into
phpstan:2.2.xfrom
SanderMuller:perf/skip-noop-result-cache-save
Closed

Skip result cache re-save when nothing changed#5843
SanderMuller wants to merge 1 commit into
phpstan:2.2.xfrom
SanderMuller:perf/skip-noop-result-cache-save

Conversation

@SanderMuller

@SanderMuller SanderMuller commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What & why

A fully warm run (no changed, new, or deleted files) rewrites the entire result cache
file even though the result is byte-identical to what is already on disk. For phpstan-src
itself that is an 11 MB (src/Type) to 26 MB (src) var_export file; for larger projects it
grows accordingly.

restore() now marks the ResultCache as up to date when nothing changed, and
process() skips the save in that case. The skip only triggers when the file list,
every file hash, and the project extension files all match the cache, so any change
still goes through the normal merge-and-save path. isSaved() reports true because the
cache on disk is current, which keeps the changed-extension-files warning behaviour
unchanged.

Benchmarks

hyperfine (--warmup 3 --runs 15), end-to-end wall time of a fully warm analyse run,
self-analysis at level 8, Apple M4 Pro, PHP 8.5.7; caches primed before measuring:

Corpus base PR Delta
src/Type (383 files, 11 MB cache) 411.2 ± 4.4 ms 386.3 ± 3.2 ms −25 ms (1.06×)
src (1707 files, 26 MB cache) 605.1 ± 11.3 ms 528.6 ± 9.2 ms −77 ms (1.14×)

Timing the skipped work directly on the base branch attributes the delta: save() takes
22.0 ms (11 MB cache) resp. 56–57 ms (26 MB cache) on a warm run, plus the
getProjectExtensionFiles() preparation the up-to-date branch also skips. The saving
scales roughly linearly with result cache size. Cold runs and runs that re-analyse files
are unaffected.

Tests

make tests (12,711, green), make phpstan, make cs, make lint and
make composer-dependency-analyser all pass. Analysis output is byte-identical,
including incremental (change + revert) runs, where the cache is correctly re-saved.

🤖 Generated with Claude Code

A fully warm run (no changed, new, or deleted files) used to rewrite the
entire result cache file - 10.5 MB of var_export output for a 383-file
project - producing a byte-identical file every time. restore() now
marks the ResultCache as up to date and process() skips the rewrite:
warm-run wall time -15%, warm CPU -24% on the benchmark corpus, scaling
with cache size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ondrejmirtes

Copy link
Copy Markdown
Member

These measurement numbers seem a lot overblown. You're not saying what part of the runtime you're measuring, it's certainly not the entire analysis time.

Please provide hyperfine benchmark time numbers with before/after for each of your changes, or Blackfire comparison links.

@SanderMuller

Copy link
Copy Markdown
Contributor Author

Fair point, and I owe you cleaner numbers — the percentages in the description came from a custom harness and I didn't state the measurement scope. Here is the full picture, measured with hyperfine.

Scope: end-to-end wall time of a fully warm run (analyse with a valid result cache, 0 files to reanalyse) — the case this PR targets. Cold runs and runs that re-analyse files are unaffected; the save still happens there.

Setup: two clean worktrees (base = ff2647a, PR = this branch), self-analysis of phpstan-src at level 8 with a minimal config, caches primed with two runs, then hyperfine --warmup 3 --runs 15. Apple M4 Pro (14 cores), PHP 8.5.7, CLI opcache off.

Command Mean [ms] Min [ms] Max [ms] Relative
base — warm run, src/Type (383 files, 11 MB cache) 411.2 ± 4.4 403.0 418.8 1.06 ± 0.01
PR — warm run, src/Type (383 files, 11 MB cache) 386.3 ± 3.2 381.0 394.1 1.00
base — warm run, src (1707 files, 26 MB cache) 605.1 ± 11.3 583.8 624.4 1.14 ± 0.03
PR — warm run, src (1707 files, 26 MB cache) 528.6 ± 9.2 513.8 550.6 1.00

So −25 ms (1.06×) on the 383-file corpus and −77 ms (1.14×) on the 1707-file corpus, with user CPU down 21 ms resp. 67 ms.

To attribute the delta I also timed the skipped work directly on the base branch (hrtime around the save() call during a warm run): 22.0 ms for the 11 MB cache and 56–57 ms for the 26 MB one. The remainder of the hyperfine delta is the getProjectExtensionFiles() preparation that the up-to-date branch skips as well.

You're right that the −15%/−24% in the description were not transferable: same absolute saving, measured against a faster-bootstrap environment, so the relative numbers looked better than they are. I've updated the description to the hyperfine numbers above. The saving grows with result cache size — the two data points suggest roughly linear in cache bytes, so a project with a 100+ MB result cache should save a few hundred ms per warm run.

I'll add hyperfine before/after numbers to the other PRs as well.

@ondrejmirtes

Copy link
Copy Markdown
Member

I don't think this one is worth it. But the other PRs might have good ideas!

@SanderMuller

Copy link
Copy Markdown
Contributor Author

I don't think this one is worth it. But the other PRs might have good ideas!

Thanks for taking the time to consider the PRs! Claude Fable seems to be able to do quite well on finding worthwhile experiments. Happy to contribute some of my usage windows to PHPStan which is invaluable to my projects

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.

2 participants