Skip to content

Restart once for Turbo, OPcache and Xdebug - #6769

Open
SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:merged-restarter
Open

SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:merged-restarter

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

With Xdebug, pcov or tideways loaded, PHPStan now restarts once, before the console application starts. This is the follow-up that #6757 (comment) describes.

Before this change, a run with Xdebug loaded did this:

  1. TurboProcessRestarter exec'd the process for Turbo and OPcache. Xdebug was still loaded.
  2. The new process booted up to CommandHelper::begin() and started a child process without Xdebug through xdebug-handler.
  3. That child had lost the -d entries, so it exec'd again for Turbo and OPcache.

Now TurboProcessRestarter makes the decision at the top of bin/phpstan, without the autoloader:

  • If Xdebug, pcov or tideways has to go, it loads the autoloader and passes its own -d entries to ProcessRestarter. The process is replaced anyway, so the late autoloader does not matter.
  • xdebug-handler writes the ini without those extensions. Then one pcntl_exec replaces the process with the Turbo, OPcache and marker entries on the command line. The command-line opcache.enable_cli=1 overrides the opcache.enable_cli=0 that xdebug-handler writes into its ini.
  • Nobody waits for the exec, so a small forked process does the job of the waiting parent. It ignores Ctrl-C, waits until the main process ends, and removes the ini.
  • On the exec path, ProcessRestarter prints the same "Process restarting" lines as xdebug-handler under XDEBUG_HANDLER_DEBUG. The pcov e2e jobs check for them.
  • CommandHelper still calls check(). In the restarted process it reads the restart settings, so the --debug note about the restart still shows. Without pcntl, it is still the fallback that restarts in a child process.

restartIfSuitable() gets two optional parameters, $autoloadPath and $appName. Rector can then do the same restart with one call: restartIfSuitable($argv, $phpstanPhar, null, 'rector'). Rector's own XdebugHandler then sees the restart and does nothing.

I tested the restart paths with a 2.3.x phar (807d030f6) that has these files patched in, on macOS with Homebrew PHP 8.5 and Xdebug 3.5:

  • With Xdebug: one exec with the same pid. The final process has Turbo and OPcache on and Xdebug off. This holds for the phar, for bin/phpstan and for a vendor/bin/phpstan proxy.
  • --xdebug, PHPSTAN_ALLOW_XDEBUG=1 and xdebug.mode=off keep Xdebug, as before.
  • With pcntl_exec or pcntl_fork in disable_functions, the old CommandHelper restart runs, and the worker gets Turbo without Xdebug.
  • After Ctrl-C to the process group, SIGINT or SIGKILL to the main process, or a normal exit, the ini is gone. Without the ignored signals in the watchdog, the ini stays after Ctrl-C.

CPU time (user + sys) of a small run with Xdebug loaded, median of 10 interleaved runs: 1.14 s before, 0.69 s after. The saving is in the startup, so I expect about the same absolute saving on a larger run, but I measured only small runs.

I could not test pcov and tideways locally. The pcov e2e jobs cover pcov.

🤖 Generated with Claude Code

SanderMuller and others added 3 commits October 20, 2026 22:51
When Xdebug, pcov or tideways has to go, TurboProcessRestarter now hands
its -d entries to ProcessRestarter. That writes the ini without those
extensions and replaces the process with pcntl_exec(), so the process
restarts once, before the console application starts. Before, PHPStan
exec'd for Turbo, booted to CommandHelper with Xdebug still loaded,
spawned a child without Xdebug, and that child exec'd again for Turbo.

Nobody waits for the exec, so the main process removes the ini on
shutdown. Without pcntl, CommandHelper still restarts in a child.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nobody waits for the exec, so a forked process takes the role of the
waiting parent of a proc_open() restart. It ignores Ctrl-C, outlives
the main process and then removes the ini. Before, a shutdown function
removed it, which did not run when a signal ended the process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
XdebugHandler prints "Process restarting" under XDEBUG_HANDLER_DEBUG in
doRestart(), which the exec skips. The pcov e2e jobs look for that line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant