feat: persistent-zval helpers (deep-copy zval trees across threads) by nicolas-grekas · Pull Request #2366 · php/frankenphp · GitHub
Skip to content

feat: persistent-zval helpers (deep-copy zval trees across threads)#2366

Merged
alexandre-daubois merged 1 commit into
php:mainfrom
nicolas-grekas:persistent-zval-helpers
May 4, 2026
Merged

feat: persistent-zval helpers (deep-copy zval trees across threads)#2366
alexandre-daubois merged 1 commit into
php:mainfrom
nicolas-grekas:persistent-zval-helpers

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Second step of the split suggested in #2287: land the persistent-zval
subsystem as a standalone, reviewable header, independent of background
workers. This is the subsystem most likely to hide latent refcount or
memory-lifetime bugs; reviewing it in isolation is higher-signal than
finding issues inside a 3k-line diff.

What

  • persistent_zval.h (renamed from the bg_worker_vars.h draft,
    prefix dropped for generality):

    • persistent_zval_validate — whitelist (scalars, arrays of allowed
      values, enum instances). Everything else fails fast.
    • persistent_zval_persist — deep-copy request → persistent (pemalloc)
      memory. Fast paths baked in: interned strings shared, opcache-
      immutable arrays passed by pointer without copying or owning.
    • persistent_zval_free — deep-free; skips interned strings and
      immutable arrays (borrowed, not owned).
    • persistent_zval_to_request — deep-copy persistent → fresh request
      memory. Enums re-resolved by class + case name on each read.
  • frankenphp.c: header included only when FRANKENPHP_TEST_HOOKS is
    defined. First real consumer (background workers) drops the guard.

  • Test hook gated on FRANKENPHP_TEST_HOOKS:

    • PHP function frankenphp_test_persist_roundtrip(mixed): mixed runs
      validate → persist → to_request → free and returns the result.
    • Registered via zend_register_functions at MINIT so it never
      appears in ext_functions[] and never ships in production builds.
  • CI workflows set -DFRANKENPHP_TEST_HOOKS in CGO_CFLAGS
    (tests.yaml + sanitizers.yaml). windows.yaml is the release
    build, not a test runner, and stays untouched.

Notes

  • Build verified both without the flag (production path, no
    unused-function warnings) and with it (test path).
  • The FRANKENPHP_TEST_HOOKS guard around the header include goes
    away in the PR that lands the first real caller; the test hook
    itself goes away in that same step once end-to-end tests cover the
    code paths.

@nicolas-grekas nicolas-grekas left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR ready, I made some minor perf optims/cleanups FYI, nothing substantial.

@zeriyoshi

zeriyoshi commented May 3, 2026

Copy link
Copy Markdown
Contributor

@nicolas-grekas

nicolas-grekas commented May 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks that's interesting. Not for this PR at this stage since we're looking for something that fits a simpler "message" style behavior, and also something that can be used on older versions of PHP.
But having something better than APCu is definitely worth experimenting!
Check the deepclone extension BTW!

@zeriyoshi

Copy link
Copy Markdown
Contributor

Thanks! I agree that maintaining broader compatibility is the better approach.

By the way, I had no idea about ext-deepcache and was developing an extension called ext-colopl_cache with almost the same structure and approach... lol

With colopl_cache, there were unsolvable performance issues — for instance, it required hooking into the Zend VM in various places — so I ended up switching to proposing engine-level changes instead. I'm planning to explain this in an RFC I'll be submitting later as well!

@alexandre-daubois alexandre-daubois left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good otherwise

Comment thread persistent_zval.h Outdated
Comment thread persistent_zval_test.go Outdated
Comment thread .github/workflows/sanitizers.yaml Outdated
Comment thread persistent_zval.h Outdated
Comment thread persistent_zval_test.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a standalone persistent_zval subsystem for deep-copying supported PHP values between request memory and persistent memory, as groundwork for the upcoming background-worker shared-state feature. It also wires in a test-only PHP entry point and enables that hook in CI so the new helper can be exercised before the real worker integration lands.

Changes:

  • Add persistent_zval.h with validate/persist/free/to-request helpers for scalars, arrays, and enums.
  • Add a test-only frankenphp_test_persist_roundtrip() hook in frankenphp.c plus PHP/Go tests that round-trip sample values.
  • Enable FRANKENPHP_TEST_HOOKS in Linux/macOS/sanitizer CI jobs so the new test path is compiled and exercised.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
testdata/persist-roundtrip.php PHP fixture that exercises supported and rejected round-trip inputs.
persistent_zval.h New header-only implementation of persistent zval validation/copy/free logic.
persistent_zval_test.go Go test that invokes the PHP round-trip fixture through FrankenPHP.
frankenphp.c Conditionally includes the helper header and registers the test-only PHP function.
.github/workflows/tests.yaml Enables the test-hook compile flag in test jobs.
.github/workflows/sanitizers.yaml Enables the test-hook compile flag in sanitizer jobs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread zval.h
Comment thread zval_test.go

@henderkes henderkes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One tiny objection: couldn't we get the test to run without the extra define? These things make it messy to replicate CI failures locally when all we ran before was "go test".

If there's no better way I'm not blocking it.

@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

couldn't we get the test to run without the extra define

the API is not used anywhere at the moment, but this is going to change very soon, and then I'll remove the extra define

Second step of the split suggested in php#2287: land the persistent-zval
subsystem as a standalone, reviewable header, independent of background
workers. This is the subsystem most likely to hide latent refcount or
memory-lifetime bugs; reviewing it in isolation is higher-signal than
finding issues inside a 3k-line diff.

- persistent_zval.h (renamed from the bg_worker_vars.h draft, prefix
  dropped for generality):
  - persistent_zval_validate: whitelist (scalars, arrays of allowed
    values, enum instances). Everything else fails fast.
  - persistent_zval_persist: deep-copy request -> persistent (pemalloc)
    memory. Fast paths baked in: interned strings shared, opcache-
    immutable arrays passed by pointer without copying or owning.
  - persistent_zval_free: deep-free; skips interned strings and
    immutable arrays (borrowed, not owned).
  - persistent_zval_to_request: deep-copy persistent -> fresh request
    memory. Enums re-resolved by class + case name on each read.

- frankenphp.c: header included only when FRANKENPHP_TEST_HOOKS is
  defined. First real consumer (background workers) drops the guard.

- Test hook gated on FRANKENPHP_TEST_HOOKS:
  - PHP function frankenphp_test_persist_roundtrip(mixed): mixed runs
    validate -> persist -> to_request -> free and returns the result.
  - Registered via zend_register_functions at MINIT so it never
    appears in ext_functions[] and never ships in production builds.

- CI workflows set -DFRANKENPHP_TEST_HOOKS in CGO_CFLAGS
  (tests.yaml + sanitizers.yaml). windows.yaml is the release build,
  not a test runner, and stays untouched.

- Build verified both without the flag (production path, no
  unused-function warnings) and with it (test path).
- The FRANKENPHP_TEST_HOOKS guard around the header include goes
  away in the PR that lands the first real caller; the test hook
  itself goes away in that same step once end-to-end tests cover the
  code paths.
@alexandre-daubois

Copy link
Copy Markdown
Member

Thanks @nicolas-grekas!

@madmajestro

Copy link
Copy Markdown

@zeriyoshi
The APCu benchmarks you mentioned are very interesting! I’m curious about which APCu version and which patterns and data were used. Could you perhaps provide a test script or something similar so that I can reproduce your results for APCu? As an APCu maintainer, I am interested in improving performance. While that is likely not possible with persistent zvals, it might be in other areas...

@nicolas-grekas

Copy link
Copy Markdown
Contributor Author

@madmajestro please check https://wiki.php.net/rfc/opcache_static_cache it has links to implementations + notes.

@zeriyoshi

Copy link
Copy Markdown
Contributor

@madmajestro
Thank you for your interest! but RFC discussion is stacked...

I thinks revert the Attribute implementation in RFC, which requires JIT intervention,
in order to reduce its scope and include it properly in PHP 8.6.

But, this will be significantly slower than a Attribute-based implementation.
(RFC Performance section: volatile_cache, pinned_cache => non-Attribute, VolatileStatic *, PinnedStatic * => Attribute)

Also, I'm trying to create PHP Extension Only implementation, that's running tests that include
various workloads, Fetch faster than 3.50x, but Store slower than 0.40x. It means this extension is NOT always better than APCu.
(This extension is compatible with PHP 8.1 and later, and since it has the potential to improve the performance of existing fetch heavy applications, I hope to public release it once it's ready.)

If you'd like, please share your thoughts on the Internals ML. I'm sure it will help move the discussion forward.
(Sorry my bad English, I think wrong sentence, maybe.)

https://externals.io/message/130912

@madmajestro

Copy link
Copy Markdown

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.

7 participants