Parallel Workers
Greph dispatches text and AST scans across a worker pool when the work is large enough to justify the overhead. The pool is built on pcntl_fork() and falls back to in-process execution when ext-pcntl is not available (Windows, some PaaS environments, restricted hosting).
Enabling parallel scans
Set -j N on the CLI or jobs: N on the relevant options object:
./vendor/bin/greph -F -j 8 "function" src
./vendor/bin/greph -p '$obj->$method($$$ARGS)' -j 4 src
./vendor/bin/rg --threads 8 -F "function" src
./vendor/bin/sg run --threads 8 --pattern '$obj->$method($$$ARGS)' src
use Greph\Greph;
use Greph\Text\TextSearchOptions;
Greph::searchText(
'function',
'src',
new TextSearchOptions(fixedString: true, jobs: 8),
);
When parallel kicks in
Greph does not parallelize unconditionally. Forking N processes and round-tripping their results through pipes is overhead, and on small file lists that overhead dominates the search itself. Instead, the facade computes a threshold from the job count, the search mode, and the pattern shape. Only file lists above the threshold are dispatched to the worker pool.
The current heuristics (in Greph\Greph::shouldUseTextWorkers and friends):
- Single-job runs (
-j 1) always run in-process. - Default text scans require roughly
jobs * 2_000files before the pool turns on. - Count, files-with-matches, and files-without-matches modes lower the threshold to roughly
jobs * 750because the per-file work is cheaper. - Short, identifier-shaped fixed-string patterns raise the threshold to roughly
jobs * 4_000because the literal fast path is already so cheap that forking rarely pays. - AST search and AST rewrite use the
jobs * 750threshold.
These numbers are heuristics, not promises. They are tuned against the WordPress benchmark corpus and may be revisited.
How the pool works
Greph\Parallel\WorkSplitter distributes the file list across N chunks. Greph\Parallel\WorkerPool forks one worker process per chunk, each worker runs the same closure on its slice, and Greph\Parallel\ResultCollector reads the serialized results back through a Unix socket pair per worker. The parent process waits for every worker, then merges and re-sorts the combined result set so the output ordering is stable regardless of how the work was split.
For text mode the worker pool uses a custom on-the-wire codec (Greph\Text\TextResultCodec) to keep the serialized payload compact. AST mode uses PHP's standard serialize().
When pcntl is unavailable
If pcntl_fork is not callable (Windows, hardened PHP builds), the pool transparently falls back to running every chunk in-process. The result is the same as -j 1. Greph does not raise an error; it just runs single-threaded.
You can detect this at runtime by checking function_exists('pcntl_fork').
Tuning advice
- Start with
-jset to half your physical core count and raise it until you stop seeing improvements. - For literal-only fixed-string searches on small repositories, single-process is usually faster than any parallel configuration.
- No published benchmark measures parallel AST search or rewrite. Measure on your own tree before settling on a job count.
- The README's "Scan Mode: Parallel Text" rows (
1,2, and4workers on the WordPress corpus) do not measure the pool: they search the short literalfunction, whose threshold ofjobs * 4_000files is above the corpus size, so every row runs in-process (#8).
Limitations
- The current pool does not support work-stealing. If one chunk is much heavier than the others, the pool waits for the slowest worker.
- Worker results are buffered until every worker finishes. Streaming output from a long-running scan (the way
rg --jsonstreams events) is not supported in parallel mode. - The parent process re-sorts results after merging, so the worker pool does not reduce peak memory usage compared to a single-process scan.
These limitations are acceptable for the workloads Greph targets (interactive code search, agent loops, CI jobs over snapshots). They will be revisited if profiling shows them as a bottleneck on real corpora.