Repository navigation
perf(instrument): bound the parser-reflection AST cache while weaving - #757
lisachenko wants to merge 4 commits into
Conversation
ReflectionEngine kept the syntax tree of every woven file and every reflected ancestor for the life of the process. The weaving pipeline now limits it when it is brought up: - SourceTransformingLoader::ensureRegistered() (also used by CacheWarmer) calls ReflectionEngine::setMaximumCachedFiles(512) (MAXIMUM_CACHED_SYNTAX_TREES), unless the application set its own limit before (detected through the uninitialized typed property, as parser-reflection has no getter). - WeavingTransformer::processSingleClass() reads the original imports from the ReflectionFileNamespace iterated in transform() instead of a new ReflectionFileNamespace(file, ns), which relied on a cache hit to avoid re-parsing the file and took the imports of the first block when a file declares the same namespace in several blocks. Cold CacheWarmer run over vendor/ (3,942 files, BenchAspect), PHP 8.5, JIT off, base c3fcd30 vs head interleaved, 6 runs each, fresh cache dir per run, medians: peak memory 678 MB -> 162 MB (-76%) warmup wall 8.94 s -> 10.33 s (+16%) warmup CPU 7.96 s -> 9.69 s (+22%) Limit candidates (head with an explicit limit, 6 interleaved runs, CPU time ratio to base): 64: 62 MB, x1.49; 256: 112 MB, x1.43; 512: 162 MB, x1.21; 1024: 240 MB, x1.25; unbounded: 678 MB, x1.07. Bounding costs time on this workload (freeing evicted trees, more GC runs, ancestors parsed again), contrary to the parse-only estimate of the issue; 512 is the best memory/time trade-off measured. Woven output is byte-identical to base. Transformation performance group, 5 interleaved runs, median ratios base -> head: rewriter default 0.102 -> 0.099, all rules 1.386 -> 1.387, WeavingTransformer 0.483 -> 0.494 (+2.1%, within the run spread). Closes #749 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
…t-cache # Conflicts: # CHANGELOG.md # src/Instrument/Transformer/WeavingTransformer.php
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #757 +/- ##
============================================
+ Coverage 97.61% 97.64% +0.02%
- Complexity 1830 1831 +1
============================================
Files 104 104
Lines 4831 4834 +3
============================================
+ Hits 4716 4720 +4
+ Misses 115 114 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…t-cache # Conflicts: # CHANGELOG.md # src/Instrument/AGENTS.md
|
Closing without merge: the bounded AST cache makes weaving slower, which defeats the purpose of #749. Measured: cold
Kept for reference: the branch Generated by Claude Code |
What and why
Fixes #749.
The change
WeavingTransformer):processSingleClass()takes theReflectionFileNamespacealready iterated intransform()and reads the original imports from it. The secondnew ReflectionFileNamespace($file, $ns)lookup is gone._files/duplicate-ns.php; it fails on master.SourceTransformingLoader::ensureRegistered()callsReflectionEngine::setMaximumCachedFiles(MAXIMUM_CACHED_SYNTAX_TREES = 512)when a container is configured.CacheWarmerand filters registered early go through this path too.src/relies on a file's AST staying cached; everything else parses by file name or works on nodes it already holds.src/Instrument/AGENTS.md(init flow, plus a rule never to rely on cached ASTs) and CHANGELOG (Performance and Fixed entries).Numbers
CacheWarmerrun overvendor/(3,942 files) with two name-based advisors, PHP 8.5, opcache on, JIT off.c3fcd30and head alternated, 6 each, fresh cache dir per run, medians.vendor/is identical to master (786 cache files, apart from the expected absolute__DIR__paths).Limit candidates (head with the limit set explicitly, 6 rounds alternating with master):
Options
memory_limitor a largevendor/inappDir.setMaximumCachedFiles()call and the constant. Warmup speed stays as on master, and the duplicate-namespace bug is still fixed.setMaximumCachedFiles()themselves.A cleaner long-term fix would be upstream in parser-reflection: a
forgetFile()API plus LRU instead of first-in-first-out eviction. The woven file's own AST could then be evicted once it is processed. An experiment with woven files not cached at all gave 128 MB at about +15–20% time.Unrelated bug found, not fixed here
WeavingTransformer::saveProxyToCache()builds the proxy path from the source file only. When one file declares several woven classes, their proxies overwrite each other in the same cache file (visible inmultiple-ns-woven.php).Tests
WeavingTransformerTest::testProxyCopiesImportsOfTheNamespaceBlockDeclaringTheClasswith the new fixture_files/duplicate-ns.php: fails on master, passes here.SourceTransformingLoaderTest: two tests in separate processes, one checking that the default limit is applied and one that a limit set by the application is kept.Checklist
type(scope): summary)composer checkpasses (phpunit after merging master: 3073 tests, phpstan level 10, php-cs-fixer, run locally)🤖 Generated with Claude Code
https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
Generated by Claude Code