Skip to content

Use work-stealing scheduler for parallel runs#1419

Closed
janedbal wants to merge 2 commits intoPHPCSStandards:4.xfrom
janedbal:work-stealing-parallel
Closed

Use work-stealing scheduler for parallel runs#1419
janedbal wants to merge 2 commits intoPHPCSStandards:4.xfrom
janedbal:work-stealing-parallel

Conversation

@janedbal
Copy link
Copy Markdown

@janedbal janedbal commented Apr 28, 2026

Summary

The current --parallel implementation pre-divides files into N equal batches up front, one per worker, then forks. With heterogeneous file processing times the worker that drew the slowest slice holds everyone up while the others sit idle.

This PR swaps the static batching for a PHPStan-style work-stealing scheduler:

  • Master keeps an ordered queue of paths and a stream_socket_pair to each worker.
  • Workers loop send "ready" (with progress for last chunk) → receive next chunk → process. When the queue drains, master sends done and the worker writes its totals + cache entries to the temp file as before.
  • Chunk size is a static 20, matching PHPStan's parallel.jobSize default.
  • Per-file progress dots/percentages now appear during parallel runs (previously: one dot per finished worker batch).

processChildProcs still merges totals and cache entries; only progress reporting moved into the new dispatchWork.

Numbers

Private monorepo backend, ~22,400 PHP files, custom rulesets, 32-core machine, --no-cache --parallel=24:

mode wall time speedup
baseline (static batching) 3:44 1.0×
work-stealing (this PR) 2:04 1.81×

Even at 24 workers on a 32-core box — plenty of CPU to absorb mild imbalance — the static-batching scheme leaves most workers idle once the unlucky ones hit slow files.

Test plan

  • vendor/bin/phpunit --filter testPhpcsParallel — 22/22 pass
  • vendor/bin/phpunit --filter ExitCode — 66/66 pass
  • Full vendor/bin/phpunit tests/ — only failure is a pre-existing unrelated .phpt fixture
  • STDIN E2E exit code paths — fixed in the follow-up commit (the endtoend.xml.dist fixture sets parallel=75, which exposed that the new worker can't see the in-memory DummyFile holding piped content; matched the existing runPHPCBF behavior of forcing parallel=1 when stdin is set)
  • bin/phpcs --parallel=4 src/ --cache=... then re-run — second run is ~180 ms (cache merged correctly across workers)
  • bin/phpcs src/Runner.php — passes the project's own coding standard

Co-Authored-By: Claude Code

The previous parallel implementation pre-divided files into N equal
batches up front, one per worker. With heterogeneous file processing
times this caused load imbalance — the worker with the slowest slice
held everyone up while the others sat idle.

Replace the static batching with a PHPStan-style work-stealing
scheduler:

- Master keeps an ordered queue of paths and a Unix socket pair per
  worker (stream_socket_pair).
- Workers loop "send ready (with progress for last chunk) → receive
  next chunk → process". When the queue drains, master sends "done"
  and the worker writes its totals + cache entries to the temp file as
  before.
- Chunk size is fixed at 20 (matching PHPStan's parallel.jobSize).
- Per-file progress dots/percentages now appear during parallel runs;
  previously you got one dot per batch.

On the project's own src/ tree, a 4-worker run goes from 8.12s to
2.36s (3.4×) on my machine. Existing testPhpcsParallel suite passes.
@jrfnl
Copy link
Copy Markdown
Member

jrfnl commented Apr 28, 2026

@janedbal While I appreciate what you are trying to do, please keep in mind that this repo strictly does not accept any AI contributions (as per the contributing guide).

The fixture for the bashunit E2E tests sets parallel=75 in the
ruleset, which used to be harmless because the old static-batching
parallel branch fetched files via $todo->current() — that returns
the in-memory DummyFile populated with the piped content.

The new work-stealing worker only receives paths and does
new LocalFile($path, ...), which fails Common::isReadable for
'STDIN', marks the file ignored, and exits with 0. Match the existing
runPHPCBF behavior and force parallel=1 when stdin is set; STDIN is a
single file so parallelizing was meaningless anyway.
@janedbal
Copy link
Copy Markdown
Author

@jrfnl Well, it is up to you, but this PR:

  • affects single file
  • brings ~50% performance boost for big codebase
  • has green CI
  • was tested in real world
  • was co-programmed with AI

I believe the value it brings is significant.

@janedbal janedbal marked this pull request as ready for review April 28, 2026 12:42
Comment thread src/Runner.php
// Same default as PHPStan's parallel scheduler — small enough that
// a fast worker keeps coming back for more, large enough to amortize
// the IPC round-trip per file.
$chunkSize = 20;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Can be made configurable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The existing bash e2e tests only exercise --parallel=2 with 2 files - each worker gets exactly one chunk, so the work-stealing loop is never actually exercised.

If we would ever continue on this POC, this should be addressed.

@janedbal
Copy link
Copy Markdown
Author

I think this idea is better - same results, much lower complexity. Closing.

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.

2 participants