Skip to content

[perf] Fork parallel workers from the main process when PHPStan Turbo is loaded - #8929

Closed
samsonasik wants to merge 5 commits into
mainfrom
perf-pcntl-fork
Closed

samsonasik wants to merge 5 commits into
mainfrom
perf-pcntl-fork

Conversation

@samsonasik

@samsonasik samsonasik commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

This PR improve parallel process performance by using pcntl_fork when possible, as a forked worker is a copy of the already initialized PHP process, so it can start working right away, see my post at dev to that I applied the performance improvement on StructArmed via pcntl_fork when possible:

https://dev.to/samsonasik/structarmed-fast-php-architecture-guards-you-can-rely-on-3be3#an-even-faster-parallel-process

Measured with bench/parallel_memory_bench.php, 8 workers, 3 cold runs each, PHP 8.4, same php.ini with PHPStan Turbo loaded on both sides:

Project Metric main (spawned workers) this PR (forked workers)
CodeIgniter4 system CPU (user+sys) 19.1–20.0 s 16.6–17.0 s
Peak total RSS 2125–2210 MiB 1974–2017 MiB
Peak single worker RSS 294–314 MiB 274–281 MiB
Changed files (fingerprint) 257 257
spiral-framework src CPU (user+sys) 11.9–12.6 s 9.7–9.8 s
Peak total RSS 1812–1889 MiB 1693–1720 MiB
Peak single worker RSS 244–258 MiB 231–236 MiB
Changed files (fingerprint) 137 137

Without PHPStan Turbo loaded in the main process, workers are still spawned and nothing changes.

@samsonasik

Copy link
Copy Markdown
Member Author

@TomasVotruba ready 👍

@TomasVotruba

Copy link
Copy Markdown
Member

I've benched this on a real Symfony project - Mautic app/bundles (245 changed files), 8 workers, cold cache, Xdebug off, PHP 8.4. PR head vs its merge-base, both with Turbo -d flags passed to the main process (otherwise no fork happens), 3 alternating runs each.

Mautic app/bundles main (spawned) this PR (forked)
CPU (user+sys) 125.0-126.3 s 120.8-123.1 s
Wall 17.0-17.8 s 16.4-16.7 s
Peak total RSS 4472-4496 MiB 4129-4258 MiB
Peak single worker RSS 575-588 MiB 542-567 MiB
Changed files (fingerprint) 245 245
  • Without Turbo in the main process: no difference (128.7 s vs 130.6 s CPU), as expected.
  • On a small subset (CoreBundle only) the gain is bigger: 34.8 s -> 26.6 s CPU, 5.7 s -> 4.6 s wall.

The fork only saves the fixed boot cost per worker. On small projects like CodeIgniter system or spiral src that is a big share of the run, but on a real-size app it's ~3 % CPU and under 1 s wall.

For that gain, this adds a second worker lifecycle - fork, waitpid polling, stderr stream filter, error log files, lazy container wiring to break the cycle - next to the spawned one, with fork-only paths that run only when Turbo is loaded. I don't think that's worth the complexity trade-off, so I'd close this one.

Thank you for the effort and the detailed measurements though 👍

@TomasVotruba

Copy link
Copy Markdown
Member

The gain here seems so tiny for such a complexity to pay off. Closing this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants