Skip to content

perf(instrument): include known files natively in production - #755

Merged
lisachenko merged 3 commits into
masterfrom
perf/746-include-map
Oct 10, 2026
Merged

lisachenko merged 3 commits into
masterfrom
perf/746-include-map

Conversation

@lisachenko

Copy link
Copy Markdown
Member

What and why

Fixes #746.

The problem

  • With Features::INTERCEPT_INCLUDES, every include/require in woven code goes through FilterInjectorTransformer::rewrite(). In production, a file with no woven copy (templates, config files, plain PHP) took this path:
    1. The full _transformation.cache was loaded.
    2. file_exists() was called on a cache path that doesn't exist.
    3. The file was included through php://filter. It was read by the stream filter, checked for freshness, and compiled on every request, because opcache never caches php://filter includes.

The change

  • Include map: _include.cache gets a files section. It maps every source known to the cache (path relative to appDir) to its cached file (__DIR__ . '/…'), or to null when the source was not transformed. CachePathManager::FORMAT_VERSION goes from 3 to 4.
    • The section is built in flushCacheState(), kept up to date within a request by setCacheState(), and emptied by clearCacheState().
    • New method CachePathManager::findIncludeFile() answers from this map only; it never loads the full metadata.
  • rewrite() in production:
    • recorded as untransformed → the original resolved path, which opcache caches;
    • recorded as woven → the recorded cached file, with no file_exists();
    • unknown → php://filter, as before.
    • Paths containing /. are resolved first, so the common __DIR__ . '/../file.php' finds its record.
  • Debug mode is unchanged.
  • Semantics: the same as for classes in the class map. A known file is trusted in production without a freshness check.
  • Compatibility:
    • An outdated PREBUILT_CACHE is still rejected with the hint to run cache:warmup:aop.
    • A moved prebuilt cache keeps working, because the keys are relative and the values are __DIR__-based.
  • Docs: CHANGELOG, the cache section of UPGRADE-4.0.md, and src/Instrument/AGENTS.md are updated.

Numbers

  • Setup: a scratch app with INTERCEPT_INCLUDES in production mode. One woven class does 20 requires of 150-line untransformed templates, plus one include of a woven file that includes another file.
  • Runs: served by php -S with opcache on and JIT off. Base c3fcd30 and head requests interleaved, 25 rounds, run twice; medians, measured in-request with hrtime.
Before After Change
20 includes of untransformed files 5021–5286 µs 210–224 µs −96%
per untransformed include 251–264 µs 10.5–11.2 µs ≈ −245 µs each
woven include (+1 nested) 231–243 µs 5.7–6.1 µs −97%
whole warm request 5874–5927 µs 652–653 µs −89%
_transformation.cache loaded on a warm request 25 of 25 0 of 25 gone

The ~11 µs per include on head still includes the one-time setup on the first rewrite() call (the stream filter is still registered there).

Transformation performance group (base vs head, 8 runs alternating the order, change in the head/base ratio): default rules −1.7%, all rules +7.6%, WeavingTransformer +0.5%.

  • Runs swing by about ±13% from load on the machine.
  • Two earlier runs that always measured head second showed WeavingTransformer at +9–11%. That fell to +0.5% once the order alternated, so it was the order and noise.
  • Nothing in this change runs during transformation: rewriteNode() is unchanged.

Behaviour notes

  • Changed include files in production: a known file is trusted without a freshness check, the same as the class map. A changed file is picked up after a cache warmup or clear.
  • Size of _include.cache: it grows by one entry per known source. opcache keeps the constant array as is; a CLI run without opcache parses a slightly bigger file.

Tests

  • FilterInjectorTransformerTest: untransformed file (relative path resolved), path with /../, woven file (never stat-ed), unknown file goes through the filter, and debug never consults the map.
  • CachePathManagerTest: the map is written and read back without the full metadata. Also covered: null entries, files outside appDir, unknown files, updates within a request, clear, and a moved cache.
  • tests/Functional/IncludeInterceptionTest.php:
    • new fixtures: FileIncluder, woven only by INTERCEPT_INCLUDES; a plain and a woven include file; bin/includes.php;
    • new configurations production_includes and moved_includes_cache, the second a prebuilt cache in a moved directory;
    • asserts the rewritten paths, that magic constants still give the original paths, the included-files list, and that _transformation.cache is not loaded;
    • fails on master, as it should.

Checklist

  • The title follows Conventional Commits (type(scope): summary)
  • Tests cover the change
  • composer check passes (phpunit: 3000 tests, phpstan level 10, php-cs-fixer, run locally)
  • User-facing or breaking changes are in CHANGELOG.md (and the upgrade guide for breaking changes)

🤖 Generated with Claude Code

https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c


Generated by Claude Code

With Features::INTERCEPT_INCLUDES every include inside woven code went
through FilterInjectorTransformer::rewrite(), which loaded the full
_transformation.cache, stat-ed a cache path that does not exist for an
untransformed file and fell back to php://filter: the file was filtered,
checked for freshness and compiled again on every request (opcache never
caches php://filter includes).

_include.cache now also carries a 'files' map: every source known to the
cache (path relative to the application root) => cached file
(__DIR__ . '/...') or null when untransformed. In production rewrite()
serves an include of a known source like the class map serves a known
class: the original file when untransformed, the recorded cached file
when woven, with no filter, no file_exists/freshness check and no load of
the full metadata. Absolute paths with relative segments
(__DIR__ . '/../x.php') are resolved before the lookup. Unknown files and
debug mode still go through php://filter. CachePathManager::FORMAT_VERSION
is bumped to 4 (an outdated PREBUILT_CACHE is rejected as before), the
map follows a moved cache directory like the class map.

Measured on a warm production request through php -S with opcache (JIT
off), 20 includes of 150-line untransformed files + 1 woven file,
base/head interleaved, 25 rounds x2, medians:
- 20 untransformed includes: 5021-5286 us -> 210-224 us (~255 -> ~11 us
  per include)
- woven include (which includes one more file): 231-243 us -> 6 us
- whole request: 5874-5927 us -> 652-653 us
- _transformation.cache loaded on a warm request: 25/25 -> 0/25
Transformation performance group (8 interleaved runs, alternating
order): all head ratios within noise (-1.7%, +7.6%, +0.5%).

Closes #746

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.79%. Comparing base (79ec2a5) to head (e4c26be).

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #755      +/-   ##
============================================
+ Coverage     97.61%   97.79%   +0.17%     
- Complexity     1830     1841      +11     
============================================
  Files           104      104              
  Lines          4831     4849      +18     
============================================
+ Hits           4716     4742      +26     
+ Misses          115      107       -8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… include

On Windows the cached file recorded for a woven include keeps the native
separators of the woven source path, so the functional test builds the
expected path with DIRECTORY_SEPARATOR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
@lisachenko
lisachenko marked this pull request as ready for review October 7, 2026 23:01
@lisachenko
lisachenko merged commit f8e80c1 into master Oct 10, 2026
15 checks passed
@lisachenko
lisachenko deleted the perf/746-include-map branch October 10, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Performance: INTERCEPT_INCLUDES serves untransformed included files through php://filter in production (no opcache)

2 participants