Skip to content

Resolve a node's file and package dependencies in one pass over unique reflections - #6475

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
faster-non-analysed-dependencies
Sep 18, 2026
Merged

ondrejmirtes merged 1 commit into
2.2.xfrom
faster-non-analysed-dependencies

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

After every analysed node, FileAnalyserCallback resolves the node's dependencies to the files and Composer packages the result cache records. Profiling bin/phpstan analyse -l 8 src/Analyser/ src/Rules/ src/Type/ --debug showed three sources of avoidable work there:

  • The reflections were walked twice. NodeDependencies::getFileDependencies() and getNonAnalysedDependencies() were called back to back on the same list. Both called getFileName() and normalizePath() on every reflection. They are now one method, getFileAndPackageDependencies(). It returns the analysed files, the non-analysed files and the packages in one pass, in the same order as before.
  • 55% of the reflections walked were duplicates (917K walked, 415K unique per node). buildClassDependencies() added getInterfaces() again at every parent level, even though it already returns inherited interfaces, and the classes one node references share most of their ancestors. DependencyResolver now keys the reflections by spl_object_id() and merges them with += instead of array_merge(). Deduplicating this way costs nothing: resolveDependencies() takes the same time as before.
  • The versioned-extension check (resolveVersionedExtensionPackage(), added in 677df31) called isBuiltin() first for every dependency, which goes through several layers of BetterReflection calls. Only ds and couchbase symbols can match. It now looks the name up in that small map first and calls isBuiltin() only on a hit.

Numbers

Measured on the command above, same session, OPcache on, without the turbo extension.

Before After
File/package resolution (both methods / the merged one) 0.72 s (1.39% of the run) 0.37 s (0.71%)
resolveDependencies() 1.18 s 1.18 s
Reflections walked 917K 415K

End to end, over 6 alternating base/head pairs, instructions retired dropped by 1.0% (432.7G → 428.3G). Every head run was below every base run (paired t ≈ −7.8). The wall-time difference (~0.4 s of ~52 s) is within the noise. The analysis output is identical to the base branch.

Verification

  • make tests passes. make phpstan and phpcs are clean.
  • All result-cache e2e scripts and the ext-ds-version scripts were run locally on the base branch and on this one. Their results match. The few scripts that fail locally fail the same way on the base branch because of the local environment.

🤖 Generated with Claude Code

https://claude.ai/code/session_0147ixdtEww7ewC6Jz6isghx

…e reflections

The reflections a node depends on were walked twice - once for the analysed
files, once for the non-analysed files and packages - normalizing every file
path both times. They are now resolved in a single pass.

The reflections are also collected keyed by their object id, so each is kept
once: every level of a class hierarchy repeated the interfaces it inherits, and
the classes one node references share most of their ancestors, so 55% of the
reflections walked were duplicates.

The versioned extension check looks the symbol up by name before asking whether
it is built-in, which goes through several layers of reflection for nearly
every dependency while nearly none of them is such a symbol.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147ixdtEww7ewC6Jz6isghx
@ondrejmirtes
ondrejmirtes merged commit 98c2226 into 2.2.x Sep 18, 2026
866 of 890 checks passed
@ondrejmirtes
ondrejmirtes deleted the faster-non-analysed-dependencies branch September 18, 2026 19:41
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.

1 participant