feat(opcache): graph-growing cache serializer (add functions/methods to a cached script) - #269
Merged
Merged
Conversation
6 tasks
The from-scratch writer sketched in issue #117 as ScriptSerializer: a two-pass port of zend_persist_calc -> zend_persist (ext/opcache/zend_persist.c, PHP-8.4.19) fused with the offset-encoding stage of zend_file_cache_serialize. Pass 1 walks the (possibly mutated) live graph from the zend_persistent_script, deduplicating every reachable allocation unit through an xlat table (the zend_shared_alloc_*_xlat_entry port) and summing ZEND_MM_ALIGNED sizes; pass 2 emits a fresh contiguous region - units copied byte-verbatim, every pointer field rewritten, late references (scopes, prototypes, hook prop_info back-references, magic-method slots, IS_INDIRECT interior pointers) resolved against the finished xlat like zend_persist.c's late lookups. The emitted region is a valid relocated image whose on-disk offset encoding is delegated to the existing PayloadRelocator serialize stage, so the offset format has exactly one implementation. Walkers cover the full 8.4 payload surface: op_arrays (static vars, literals, opcodes, arg_info incl. the arg_info[-1] return slot, vars, live ranges, try/catch, attributes, dynamic_func_defs), classes (unlinked and linked-parent, constants, properties incl. hooks, interface/trait names, aliases, precedences, iterator/arrayaccess funcs), zvals/arrays/constant ASTs, warnings and early bindings. script->size is re-stamped (it is the loader's IS_SERIALIZED bound); zend_hash_persist's sparse-table compaction is deliberately skipped (optimization only) and every string is region-copied exactly like a file_cache_only child (nothing is accel-interned there), stamping zend_set_str_gc_flags' interned bits on sources that lack them. The API seam: ReflectionOpcacheFile::addFunctionFrom()/addMethodFrom() graft op_arrays from DONOR cache binaries (compiled by a real opcache child, so their oplines are already file-form - handler-table indexes and literal-index operands are not derivable in-process without unexported engine helpers), regrowing the target hashtable outside the buffer with a faithful re-implementation of the persisted-table insert (hash slots ahead of arData, bucket-index chains, HT_SIZE_TO_MASK = -(2*size); persisted data blocks must never be touched by zend_hash_add). BinaryCacheFile::save() routes grown graphs through the serializer automatically and keeps the byte-exact derelocate() path for in-place edits. Whole added classes and in-process compiled op_arrays remain out of scope and are refused loudly. Also fixes a latent relocator bug found by the serializer's byte checks: _ZSTR_HEADER_SIZE is XtOffsetOf(zend_string, val) = sizeof - 8, not sizeof - 1, which made emitInterned over-copy 7 bytes per emission. Acceptance evidence (GraphGrowingSerializerTest): - issue #117 acceptance: a brand-new function AND a new method grafted into the cached answer.php execute from the file cache in fresh workers ('added-fn', 'added-method-ok'), alongside the original entries; the grown binary passes checksum and round-trips byte-identically through the relocator - rebuild coverage: all seven fixture payloads (attributes/statics, type lists, traits, closures, property hooks, iterators, jump/const probe) re-emitted from scratch execute from the cache and round-trip byte-identical - refusal paths: unknown donor entries and duplicate keys throw dedicated OpCacheException factories - full default suite (530 tests, baseline skips), --group opcache --fail-on-skipped OK (58 tests, 437 assertions) on host, debug84 and debug84-zts containers; phpstan level max clean (the two new pointer-surgery zones carry the same scoped ignores as PayloadRelocator); php-cs-fixer clean Fixes #117 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDcCQiYqbMkjRPyhWgLL6M
lisachenko
force-pushed
the
claude/opcache-119-darwin-relocator
branch
from
August 20, 2026 00:30
193bd6e to
a20e806
Compare
lisachenko
force-pushed
the
claude/opcache-117-graph-serializer
branch
from
August 20, 2026 00:30
6a4bc8f to
08ed9b7
Compare
6 tasks
lisachenko
marked this pull request as ready for review
August 20, 2026 00:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Fixes #117 — stacked on #268. The largest deferred relocator item: serialize a mutated, graph-grown script (not just in-place edits) back to a valid binary.
New
src/OpCache/ScriptSerializer.php: a two-pass port ofzend_persist_calc→zend_persist(xlat dedup + aligned sizing; byte-verbatim unit copies + pointer rewrites + deferred late-reference resolution including IS_INDIRECT interior pointers), delegating offset encoding to the existingPayloadRelocatorserialize stage. Full 8.4 payload surface covered.BinaryCacheFile::save()auto-routes grown graphs through the serializer and keeps byte-exactderelocate()for in-place edits. Includes a faithful persisted-table hashtable regrowth (persisted data blocks must never be touched byzend_hash_add).API seam:
ReflectionOpcacheFile::addFunctionFrom()/addMethodFrom()graft op_arrays from donor cache binaries — their oplines are already in file form, whereas in-process-compiled op_arrays can't be re-serialized faithfully without unexported engine helpers (that stays the deliberate boundary, refused loudly).Bonus fix (surfaced by the serializer's byte checks):
_ZSTR_HEADER_SIZEissizeof − 8(theXtOffsetOfvalue), notsizeof − 1—emitInternedwas over-copying 7 bytes per interned string inPayloadRelocator.Acceptance met: a grafted function (
added-fn) and method (added-method-ok) execute from the file cache in fresh workers alongside the originals; all 7 fixture payloads rebuild from scratch and execute; the grown binary round-trips byte-identical.Environment it was verified on
php -v): PHP 8.4.19 (cli) (built: Mar 30 2026 19:28:35) (NTS) + ZTS debug container--enable-debug)? yes — debug84 NTS + debug84-zts, opcache gate 58 eachAlso: host 530 tests, opcache gate 58; PHPStan level max clean; cs-fixer clean.
Checklist
8.4)composer testpasses on the matching PHP minorcomposer phpstan(level max) andcomposer cs:checkare greentools/generator/symbols.phpunchanged — nothing generated touched🤖 Generated with Claude Code
https://claude.ai/code/session_01BDcCQiYqbMkjRPyhWgLL6M
Generated by Claude Code