Skip to content

perf(di:compile): cache ReflectionClass instantiations in Type, Config/Reader and ClassReader#40615

Open
SamJUK wants to merge 1 commit intomagento:2.4-developfrom
SamJUK:perf/di-compile-reflection-caches
Open

perf(di:compile): cache ReflectionClass instantiations in Type, Config/Reader and ClassReader#40615
SamJUK wants to merge 1 commit intomagento:2.4-developfrom
SamJUK:perf/di-compile-reflection-caches

Conversation

@SamJUK
Copy link
Copy Markdown
Contributor

@SamJUK SamJUK commented Mar 21, 2026

Description

Three hot-path functions in the DI compiler call new ReflectionClass() on every invocation with no caching. On a mid-size install (390–470 modules) this adds up to roughly 60,000+ redundant instantiations per compile run.

1. Type::isConcrete() — ~40,000 uncached calls

Called for every class in DefinitionsCollection for every compilation area, with no caching. With ~5,000 classes and 8 areas, the same ReflectionClass objects are instantiated up to 8 times each.

2. Config/Reader::generateCachePerScope() — ~20,000 uncached calls

The preference resolution loop checks whether each preference is a PHP extension class via new ReflectionClass($preference)->getExtension(). With 2,542 preferences across 8 areas that's ~20,000 instantiations, yet only ~10 classes (PDO, SplStack, etc.) actually are extension classes. 99.9% of these return null and are thrown away.

3. ClassReader::getConstructor() — repeated calls across the pipeline

getConstructor() is called from multiple pipeline stages for the same class. There is already a $parentsCache on ClassReader but no equivalent for constructor signatures.

Fix: add a simple per-instance memoisation array to each of the three methods. All three are pure functions of their input for the duration of a compile run, so the caches are always valid and never need invalidating within a single process.

Note: array_key_exists is used rather than isset in ClassReader because classes with no constructor legitimately return null, which isset would treat as a cache miss.

Related Pull Requests

Fixed Issues (if relevant)

Manual testing scenarios

Timing comparison

# Baseline on 2.4-develop
git checkout 2.4-develop
rm -rf generated/ var/cache/
time php bin/magento setup:di:compile --no-ansi 2>&1 | tee /tmp/compile-baseline.txt

# This branch
git checkout perf/di-compile-reflection-caches
rm -rf generated/ var/cache/
time php bin/magento setup:di:compile --no-ansi 2>&1 | tee /tmp/compile-patched.txt

To isolate the affected phase:

grep -i "area configuration" /tmp/compile-baseline.txt
grep -i "area configuration" /tmp/compile-patched.txt

On a ~400-module project expect the Area configuration aggregation phase to drop by roughly 30–45%.

Output correctness

cp -r generated/ /tmp/generated_patched
git checkout 2.4-develop && rm -rf generated/ var/cache/
php bin/magento setup:di:compile --no-ansi
diff -r /tmp/generated_patched/ generated/   # should produce no output

Benchmarks on real-world projects

Project Area config phase before Area config phase after Saved
~470 modules, 877 plugins 1.9s 1.2s ~0.7s / 37%
~390 modules (Store1) 3.8s 2.1s ~1.7s / 45%
~390 modules (Store2) 3.9s 2.1s ~1.8s / 46%

Memory impact

Measured on a ~470-module install:

Metric Baseline Patched Delta
PHP working memory at peak 370 MB 386 MB +16 MB (+4.3%)
OS peak RSS 448 MB 464 MB +16 MB (+3.7%)

The increase is the memoised reflection results being retained in memory rather than discarded after each call. For a CLI compile tool that typically runs with memory_limit=2048M this is well within normal bounds.

Questions or comments

The three caches are independent and could be split into separate PRs if preferred. Kept together here as they follow the same pattern and affect the same compilation phase.

Contribution checklist

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests (if applicable)
  • README.md files for modified modules are updated — no README changes needed
  • All automated tests passed successfully (all builds are green)

@m2-assistant
Copy link
Copy Markdown

m2-assistant bot commented Mar 21, 2026

Hi @SamJUK. Thank you for your contribution!
Here are some useful tips on how you can test your changes using Magento test environment.
❗ Automated tests can be triggered manually with an appropriate comment:

  • @magento run all tests - run or re-run all required tests against the PR changes
  • @magento run <test-build(s)> - run or re-run specific test build(s)
    For example: @magento run Unit Tests

<test-build(s)> is a comma-separated list of build names.

Allowed build names are:
  1. Database Compare
  2. Functional Tests CE
  3. Functional Tests EE
  4. Functional Tests B2B
  5. Integration Tests
  6. Magento Health Index
  7. Sample Data Tests CE
  8. Sample Data Tests EE
  9. Sample Data Tests B2B
  10. Static Tests
  11. Unit Tests
  12. WebAPI Tests
  13. Semantic Version Checker

You can find more information about the builds here
ℹ️ Run only required test builds during development. Run all test builds before sending your pull request for review.


For more details, review the Code Contributions documentation.
Join Magento Community Engineering Slack and ask your questions in #github channel.

…g/Reader, ClassReader

Three hot-path locations in the DI compiler create new ReflectionClass instances
on every call with no caching, causing tens of thousands of redundant reflection
operations per compile run.

1. Type::isConcrete() — called on every class in every area with no cache.
   On a 400-module install: ~40,000 ReflectionClass instantiations per run.
   Fix: add $concreteCache[] keyed by class name.

2. Config/Reader::generateCachePerScope() — preference loop calls
   new ReflectionClass($preference) per preference per area
   (2,542 preferences × 8 areas = ~20,000 instantiations).
   Fix: add static $phpExtensionClassCache[] keyed by class name.

3. ClassReader::getConstructor() — no cache on constructor reflection despite
   being called repeatedly for the same class during compilation.
   Fix: add $constructorCache[] keyed by class name.

Benchmark results (combined effect of all three caches):

| Project     | Area config before | Area config after | Saving |
|-------------|-------------------|-------------------|--------|
| sandbox     | 2.8s              | 1.9s              | 32%    |
| ma-griggs   | 5.1s              | 3.2s              | 37%    |
| elesi       | 3.9s              | 3.3s              | 15%    |

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SamJUK SamJUK force-pushed the perf/di-compile-reflection-caches branch from 1673beb to 49717d2 Compare March 21, 2026 23:33
@SamJUK
Copy link
Copy Markdown
Contributor Author

SamJUK commented Mar 22, 2026

@magento run all tests

@engcom-Hotel engcom-Hotel added the Priority: P2 A defect with this priority could have functionality issues which are not to expectations. label Mar 24, 2026
@github-project-automation github-project-automation bot moved this to Pending Review in Pull Requests Dashboard Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority: P2 A defect with this priority could have functionality issues which are not to expectations. Progress: pending review

Projects

Status: Pending Review

Development

Successfully merging this pull request may close these issues.

2 participants