Skip to content

Commit 47f7b00

Browse files
ondrejmirtesclaude
andcommitted
Activate OPcache in the restarted process and fork workers under it
ForkParallelChecker now blocks fork only on JIT. Concurrent population of plain OPcache's shared memory from forked children is php-fpm's normal operating model (writes go through the allocator lock, readers are lock-free), and the mid-request fork cannot hold that lock or ever execute a cache restart — only JIT's shared code buffer is fork-unsafe. TurboProcessRestarter piggybacks -d opcache.enable=1 + enable_cli=1 onto the exec it pays for anyway: optimized opcodes, the inheritance cache, and forked workers inheriting the parent's warm SHM instead of each compiling lazily-loaded classes for itself. JIT is pinned off via both opcache.jit=disable and jit_buffer_size=0 — one directive alone leaks on one generation of defaults (<= 8.3 defaults jit=tracing, so an ini with a web-tuned buffer size would JIT; >= 8.4 defaults the buffer to 64M, so an ini setting just opcache.jit would). It is stripped even when the ini enables it deliberately — a measured slowdown for PHPStan that would also force spawned workers — the same treatment the xdebug-handler restart gives xdebug. When OPcache is not loaded (possible only on <= 8.4; since 8.5 it is always built in) nothing is added: -d zend_extension= would only emit startup warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ek2A83bWb3Yf4CRmtvBfi4
1 parent ab63c95 commit 47f7b00

2 files changed

Lines changed: 57 additions & 3 deletions

File tree

‎src/Parallel/ForkParallelChecker.php‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030
* lost by forking either: the distributed binaries only exist next to a
3131
* phar, so spawned workers of a source checkout run without turbo too —
3232
* an ini-loaded extension is inherited by fork like any other.
33-
* - OPcache + JIT off — their shared memory is not safe to populate
34-
* concurrently from forked children and doing so corrupts analysis
35-
* results.
33+
* - JIT off — its shared code buffer is not safe to populate concurrently
34+
* from forked children. Plain OPcache is fine: concurrent population of
35+
* its SHM under the allocator lock is php-fpm's normal operating model,
36+
* and TurboProcessRestarter deliberately activates it for the restarted
37+
* process (with JIT pinned off).
3638
*
3739
* A forked worker ends with the extension's _exit() instead of PHP's full
3840
* teardown (see ForkedChildTerminator): the child inherits every loaded

‎src/Turbo/TurboProcessRestarter.php‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
* running from a phar) the phar-fork-guard that keeps phar:// reads safe
2727
* across fork.
2828
*
29+
* The restarted process also gets OPcache activated — with JIT pinned off,
30+
* whatever the user's ini says — so forked workers share the parent's warm
31+
* opcode cache (see getOpcacheArgs()).
32+
*
2933
* The restart carries a marker ini entry with the extension path. It doubles
3034
* as the retry stop (a binary that failed to load leaves the extension
3135
* unloaded — without the marker the restart would loop) and tells
@@ -91,6 +95,10 @@ public static function restartIfSuitable(array $argv): void
9195
}
9296
$args[] = '-d';
9397
$args[] = 'memory_limit=' . ini_get('memory_limit');
98+
foreach (self::getOpcacheArgs() as $opcacheArg) {
99+
$args[] = '-d';
100+
$args[] = $opcacheArg;
101+
}
94102
$args[] = '-d';
95103
$args[] = 'extension=' . $extensionPath;
96104
$args[] = '-d';
@@ -103,4 +111,48 @@ public static function restartIfSuitable(array $argv): void
103111
// pcntl_exec() returns only on failure — continue without the extension
104112
}
105113

114+
/**
115+
* OPcache directives for the restarted process, as `name=value` ini entries.
116+
*
117+
* The exec is paid for anyway, so it doubles as the chance to activate
118+
* OPcache, usually dormant on CLI (opcache.enable_cli defaults to off):
119+
* optimized opcodes and the inheritance cache speed up the whole run, and
120+
* the pcntl_fork()ed workers inherit the parent's warm shared memory
121+
* instead of each compiling lazily-loaded classes for itself. Concurrent
122+
* population of that inherited cache is safe — it is php-fpm's normal
123+
* operating model (see ForkParallelChecker).
124+
*
125+
* JIT is always pinned off, even when the user's ini enables it — it is a
126+
* measured slowdown for PHPStan's workload and its shared code buffer is
127+
* not fork-safe, so honoring it would cost twice (the same treatment the
128+
* xdebug-handler restart gives xdebug). Merely flipping
129+
* opcache.enable_cli=1 could also activate it behind the user's back: on
130+
* PHP <= 8.3 opcache.jit defaults to `tracing`, so an ini setting just a
131+
* non-zero opcache.jit_buffer_size (common web-tuning advice) suddenly
132+
* JITs; on PHP >= 8.4 the buffer defaults to 64M, so an ini setting just
133+
* opcache.jit does
134+
* (https://php.watch/versions/8.4/opcache-jit-ini-default-changes).
135+
* Pinning both directives covers both generations of defaults.
136+
*
137+
* The only case adding nothing is OPcache not being loaded at all — real
138+
* on PHP <= 8.4, gone on 8.5+ (always built in and loaded). Loading it
139+
* from here is not worth it: -d zend_extension=opcache emits a startup
140+
* warning on builds without the shared object, and on 8.5+ always.
141+
*
142+
* @return list<string>
143+
*/
144+
private static function getOpcacheArgs(): array
145+
{
146+
if (!extension_loaded('Zend OPcache')) {
147+
return [];
148+
}
149+
150+
return [
151+
'opcache.enable=1',
152+
'opcache.enable_cli=1',
153+
'opcache.jit=disable',
154+
'opcache.jit_buffer_size=0',
155+
];
156+
}
157+
106158
}

0 commit comments

Comments
 (0)