parallel.loadLimit never reads the system load average
Bug report
parallel.loadLimit never reads the system load average. The config reference describes it as a fraction of the idle cores, but the shipped behaviour is a fraction of the total cores.
The config reference says:
The number of available cores is determined by the system load average — busy cores are subtracted from the total.
For example, if the system has 11 cores and 5 are busy, there are 6 available cores. Setting
loadLimitto0.7means PHPStan will use 70% of 6 = 4 cores for parallel workers.Note that
1.0does not mean "no limit" — it means 100% of the available cores, not the total number of cores on the machine.
Code snippet that reproduces the problem
parameters:
level: 0
paths:
- app
parallel:
loadLimit: 1.0Run phpstan diagnose -c <that file> on a machine that is busy, and read the Usable CPU cores line. PHPStan 2.2.13 from the phar, 14 cores, load average recorded per run:
loadLimit=1.0 load1min=6.19 Detected CPU cores: 14 Usable CPU cores: 14
loadLimit=0.75 load1min=6.19 Detected CPU cores: 14 Usable CPU cores: 10
loadLimit=0.5 load1min=5.77 Detected CPU cores: 14 Usable CPU cores: 7
loadLimit=0.25 load1min=5.77 Detected CPU cores: 14 Usable CPU cores: 3Expected output
The documented formula subtracts the busy cores first. Asking the library for both, so the comparison is its own arithmetic and not mine:
loadLimit load documented actual
1.0 6.19 7 14
0.75 6.19 5 10
0.5 5.77 4 7
0.25 5.77 2 3Actual output
The right-hand column above: floor(loadLimit * 14) every time. The result does not change with the load.
Where it comes from
CpuCoreCounter in phpstan/phpstan-src (src/Process/CpuCoreCounter.php:81) calls the library with three arguments:
(new FidryCpuCoreCounter())->getAvailableForParallelisation(0, null, $this->loadLimit)The fourth parameter is ?float $systemLoadAverage = 0., and the library only calls sys_getloadavg() when it receives null there. With the default the load term is zero, so loadLimit * ($availableCores - 0) is a flat percentage of the total.
The library's own result object reports which value it used. Real load 5.77 at the time:
3 arguments, as PHPStan calls it availableCpus=14 correctedSystemLoadAverage=0.0
4 arguments, passing null availableCpus=8 correctedSystemLoadAverage=5.767578125This has been the behaviour since phpstan/phpstan-src@89f1382, released in 2.1.41.
Why it matters
In #14505 someone asked for a way to stop concurrent PHPStan runs saturating their machine, and the answer was to tune loadLimit. Tuning it does cap the worker count, so that user got what they needed. It is not what they were told they were getting. They have a fixed percentage of all cores, not protection that responds to load.
What I am not asking for
The parameter is not broken as a core-count limiter, and I am not proposing to pass null. That would read a 1-minute average against runs that finish in 20 seconds. It also subtracts load from cores one for one, which turns a second concurrent run into a single worker. The worker count is fixed before the first worker spawns, so it never recovers when the first run ends.
Correcting the config reference looks like the right fix, and possibly the parameter name.
Source: phpstan/phpstan