feat(web): worker modes, EIR exception/GC correctness, and the opt-in web-scaling stack#456
Draft
Guikingone wants to merge 1 commit into
Draft
feat(web): worker modes, EIR exception/GC correctness, and the opt-in web-scaling stack#456Guikingone wants to merge 1 commit into
Guikingone wants to merge 1 commit into
Conversation
--web-worker=script non-intrusive worker mode + EIR exception/GC correctness
Guikingone
marked this pull request as draft
July 4, 2026 18:08
Guikingone
force-pushed
the
feat/web-worker-script
branch
2 times, most recently
from
July 4, 2026 20:51
c2af4dd to
ae0907d
Compare
Collaborator
Author
|
Hi @nahime0 👋🏻 This PR is highly experimental and inspired by #409 / FrankenPHP, I'm not totally convinced by the CLI options so don't consider the code as polished for now, the main goal is to introduce a "worker mode" that can either be "intrusive through the added function" or "non-intrusive" if you prefer to use Elephc only for production environnements while relying on FPM / Nginx on local ones. It also brings native fixes for already implemented code as the worker mode forced me to debug exceptions and so on 😓 PS: No benchmarks for now as I'm trying to stabilize the code, a benchmark is planned once it's stable. |
Guikingone
force-pushed
the
feat/web-worker-script
branch
from
July 5, 2026 16:45
904f9de to
afc0c20
Compare
--web-worker=script non-intrusive worker mode + EIR exception/GC correctness
Guikingone
force-pushed
the
feat/web-worker-script
branch
2 times, most recently
from
July 9, 2026 21:42
8b55e94 to
b37a177
Compare
Adds the --web-worker=script mode, boot-before-fork optimization, and associated runtime correctness fixes for the elephc web server. Web worker modes: - --web-worker=script: re-executes the top-level per request with persistent statics/globals (no register API needed, portable PHP) - --web-worker (handler): boot PHP once in the master before fork; workers inherit the boot state via kernel copy-on-write Boot-before-fork: - elephc_web_worker_register() returns instead of diverging - Workers enter enter_worker_loop directly, inheriting boot state via COW - exit()/die() during boot longjmps back to the master (exit boundary) - elephc_worker_register() is a no-op in CLI mode (same file runs everywhere: php-cli, elephc CLI, elephc --web-worker) Immortal GC bit (0x40): - __rt_end_boot_phase marks all live heap blocks as immortal after boot - Cycle collector skips immortal blocks in all passes (AArch64 + x86_64) - decref_any skips immortal blocks (never freed by per-request release) - macOS C-ABI alias stub for ___rt_end_boot_phase with .no_dead_strip ABI fix: - nested_call_reg (x19/r12) unconditionally saved in every EIR function prologue and in the legacy runtime callable invoker FD cleanup: - Workers close all inherited FDs except stdio and boot-pipe after fork Per-request cost reductions: - Superglobals built only when referenced (lazy detection) - TCP_NODELAY, gzip fast level, CString-direct header collection - Request body kept as refcounted Bytes - HTTP connection builder reused per worker - $_ENV built once at boot in handler mode Mixed-boxed COW: - Cell-rc-aware COW for static array append (fixes O(n^2) clone+leak) - __rt_array_uncow_if_cell_unique + __rt_mixed_slot_publish helpers Deterministic interface/class ID assignment (sorted keys before ids)
Guikingone
force-pushed
the
feat/web-worker-script
branch
from
July 15, 2026 07:26
b37a177 to
be229e4
Compare
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.
Summary
This PR consolidates the
--webstack for elephc: the persistent-worker execution model, the EIR exception/GC correctness that makes long-lived workers leak-free, a per-request performance pass across all web modes, and the opt-in web-scaling stack (keep-alive, TLS, master fd-dispatch, handler offload, HTTP/2, RSS recycle, SIGHUP reload, metrics, static-asset fast path, worker CPU affinity). Every scaling flag is opt-in and OFF by default — the hot path is byte-for-byte unchanged when a flag is absent.Web execution models
Three web modes share the prefork model (one process per worker, SO_REUSEPORT or master fd-dispatch, single-threaded I/O per worker, N=1 PHP handler per worker):
--web— classic prefork. Every request re-runs the whole top-level PHP and resets all state (function statics, static properties, globals, superglobals). Mirrors PHP-FPM: fully isolated per request, no shared state.--web-worker(handler) — FrankenPHP-style. The top-level runs once at boot to build long-lived state, then hands a handler closure to the runtime viaelephc_worker_register($handler), invoked per request. Statics / static properties / globals persist for the worker's lifetime.--web-worker=script(non-intrusive) — the same persistence, but on unmodified, standard PHP with no proprietary registration API. The whole top-level is re-executed per request (like--web) while statics / static properties / globals persist (like the handler flavor), so a boot-once cache is expressed in pure, portable PHP:The script stays 100% standard PHP — still runnable under php-fpm /
php -S.exit()/die()get a proper request-end bailout instead of killing the worker.Request-scoped state (
$_SERVER,$_GET,$_POST,$_FILES,$_COOKIE,$_REQUEST,php://input) is rebuilt fresh each request in every mode.Long-lived-worker GC correctness
A persistent worker only survives if request churn is leak-free, so this PR lands the EIR ownership/GC fixes that make that true:
throw/exit/dierelease each unwound frame's owned refcounted locals. EIR emits per-function cleanup callbacks walked via the_exc_call_frame_topchain.getMessage()/__toString()return independent copies, soobject_free_deepfrees the message exactly once. The x86_64decref/freepaths accept heap kind 6 (throwables).Mixed→Objectunbox incref is gated onOwnedownership, so a borrowed receiver load is not over-retained.parent::__construct()on builtin Throwable subclasses — lowered to an inline message/code field stamp on$thisinstead of a call to the never-emitted builtin constructor symbol.staticnull-guard SIGSEGV fix (so the boot-once idiom works at top level, not only inside a function).Per-request performance pass (all web modes)
No behavior change (a program that reads every superglobal behaves exactly as before):
$_ENVbuilt once per worker in--web-worker(handler);--weband--web-worker=scriptkeep$_ENVper-request fresh.TCP_NODELAY; response gzip at the fast level;HTTP_*$_SERVERkeys precomputed in Rust; request body kept refcounted instead of copied; connection builder constructed once per worker; redundant header/protocol string copies removed.Web-scaling stack (opt-in, all 3 web modes)
12 commits on top of the worker-mode work. Each flag is opt-in, OFF by default, and bench-verified no-regression at each tier.
Concurrency & dispatch
--max-requests-per-connection,--idle-timeout) — opt-inConnection: closeafter N + idle watchdog. Defaults 0/0 (rotation hurts mixed p99 in the bench;--dispatch masteris the real load-distribution fix).--dispatch master|kernel, default kernel) — the master accepts and hands accepted fds to preforked workers over a unix-domain socket, so SO_REUSEPORT's uneven kernel distribution doesn't starve workers.--handler-offload,--max-pending) — runs the blocking PHP handler on ONE dedicated thread per worker, fed by a bounded mpsc queue, so I/O of other connections overlaps PHP execution. The N=1 lever that gives I/O-overlap without ZTS. Queue-full →503+Retry-After: 1.--http2,--http2-max-streams,--http2-max-header-size) — h2c prior-knowledge; auto-requires--handler-offload(multiplexed streams stall on a single inline handler). ALPN h2-over-TLS wired (["h2","http/1.1"]vs["http/1.1"]).Ops & robustness
--tls-cert,--tls-key) — rustls 0.23;$_SERVER['HTTPS']via a C-ABI getter.--max-rss MiB) — a worker whose VmRSS exceeds the cap self-exits to be respawned (Linux/proc/self/status+ macOSmach_task_basic_info).--reload-grace) —SIGUSR1graceful drain vsSIGTERMhard; rolling one-at-a-time restart in both kernel and master dispatch.--metrics,--metrics-path /_status) — per-worker JSON snapshot (rps, latency p50/p99, status classes, 503 rate, RSS, active conns, h2 streams).--static-dir,--static-prefix,--static-cache-size,--static-max-age,--static-max-file-size) — serves files from a dir on the I/O thread before the PHP handler runs; per-worker LRU; ETag304; path-traversal404. Usesspawn_blocking+std::fs(notokio::fsdep).--worker-affinity) — pins each worker to CPUgetpid() % ncpus(round-robin via consecutive PIDs). Linux: hardsched_setaffinity; macOS: advisorythread_policy_settag (no hard pin). Best-effort, once at startup.Hygiene
crates/elephc-globals-audit/extracts all mutable.comm/.globl+ bridge statics, classifies CONST/RO_BOOT/SHARED_LOCK/TLS, versioned baselines per 3 targets,--checkmode fails on a new unclassified global (silent-miscompilation gate). ~39 rawemitter.adrpoutliers centralized. Ships an addressing-mode microbench (input to the ZTS decision below).Concurrency model — final state & the ZTS decision
The concurrency unit is N=1 PHP handler per worker process. The scaling levers above tune how many workers, how they're dispatched, and how the I/O thread overlaps with the single handler — without a thread-safe runtime.
Why no ZTS (thread-safe runtime)? A data-driven probe settled this:
--max-pendingbound (default 16) rejects overflow fast to protect the fast route's latency. Raising it to 128 drives 503s to 0 but collapses throughput to 332 rps p99 200ms (everything queues behind slow).native_tls(__thread, instruction-identical to the currentadrp+add/RIP-relative baseline, measured equal) — NOT a reserved context register (2.5× slower in the microbench, and far more invasive). The build-vs-abandon question is settled by the scaling probe: abandon ZTS.So the concurrency story is: prefork N workers × 1 handler + opt-in offload for I/O overlap +
--max-pendingshedding + scale workers to load. The knobs (--workers,--max-pending,--max-rss,--worker-affinity,--dispatch) cover the design space without a thread-safe runtime.Tests
3-target (macos-aarch64 host; linux-x86_64 + linux-arm64 in CI).
web_testsis 151 tests (worker + script modes, the perf pass, and the full scaling stack: keep-alive / TLS / fd-dispatch / offload / HTTP2 / ALPN / RSS / SIGHUP / metrics / static-asset / affinity), plusexceptionsandruntime_gcfor the EIR GC correctness, and theparent::__constructregressions. Updated examples and docs; behavior verified byte-identical to PHP where applicable. Zero compiler warnings;git diff --checkclean.No-regression benches (hello + mixed, 4 workers, release): Tier 1 hello 35234 rps p99 2.59ms / mixed 357.86 rps p99 182ms; Tier 2 hello 34922 rps p99 2.21ms / mixed 375.45 rps p99 186ms — both within the established noise band, 0 errors. The opt-in flags are byte-for-byte off by default.