Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ parameters:
parallel:
jobSize: 20
processTimeout: 600.0
maximumNumberOfProcesses: 8
maximumNumberOfProcesses: auto
minimumNumberOfJobsPerProcess: 2
buffer: 134217728 # 128 MB
loadLimit: 1.0
Expand Down
2 changes: 1 addition & 1 deletion conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ parametersSchema:
parallel: structure([
jobSize: int(),
processTimeout: float(),
maximumNumberOfProcesses: int(),
maximumNumberOfProcesses: anyOf(int(), 'auto'),
minimumNumberOfJobsPerProcess: int(),
buffer: int(),
loadLimit: schema(float(), nullable())
Expand Down
41 changes: 41 additions & 0 deletions src/Diagnose/SystemResourcesDiagnoseExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace PHPStan\Diagnose;

use PHPStan\Command\Output;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Process\CpuCoreCounter;
use PHPStan\Process\SystemResources;
use function sprintf;

/**
* Reports what PHPStan believes about the machine, so a user who disagrees with the
* number of workers it chose can see which input was wrong.
*/
#[AutowiredService]
final class SystemResourcesDiagnoseExtension implements DiagnoseExtension
{

public function __construct(
private CpuCoreCounter $cpuCoreCounter,
private SystemResources $systemResources,
)
{
}

public function print(Output $output): void
{
$output->writeLineFormatted('<info>System resources:</info>');
$output->writeLineFormatted(sprintf('Detected CPU cores: %d', $this->cpuCoreCounter->getDetectedNumberOfCpuCores()));

$quota = $this->systemResources->getCpuQuota();
$output->writeLineFormatted(sprintf(
'cgroup CPU quota: %s',
$quota === null ? 'none' : sprintf('%d cores', $quota),
));

$output->writeLineFormatted(sprintf('Usable CPU cores: %d', $this->cpuCoreCounter->getNumberOfCpuCores()));
$output->writeLineFormatted('');
}

}
35 changes: 29 additions & 6 deletions src/Parallel/Scheduler.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@
final class Scheduler implements DiagnoseExtension
{

/** @var array{int, int, int, int}|null */
public const AUTO = 'auto';

/** @var array{int, int, int, int, string}|null */
private ?array $storedData = null;

/**
* @param positive-int $jobSize
* @param positive-int $maximumNumberOfProcesses
* @param positive-int|self::AUTO $maximumNumberOfProcesses
* @param positive-int $minimumNumberOfJobsPerProcess
*/
public function __construct(
#[AutowiredParameter(ref: '%parallel.jobSize%')]
private int $jobSize,
#[AutowiredParameter(ref: '%parallel.maximumNumberOfProcesses%')]
private int $maximumNumberOfProcesses,
private int|string $maximumNumberOfProcesses,
#[AutowiredParameter(ref: '%parallel.minimumNumberOfJobsPerProcess%')]
private int $minimumNumberOfJobsPerProcess,
)
Expand Down Expand Up @@ -78,25 +80,46 @@ public function scheduleWork(
$cpuCores,
);

$usedNumberOfProcesses = min($numberOfProcesses, $this->maximumNumberOfProcesses);
$this->storedData = [$cpuCores, count($files), count($jobs), $usedNumberOfProcesses];
[$maximumNumberOfProcesses, $decision] = $this->resolveMaximumNumberOfProcesses($cpuCores);
$usedNumberOfProcesses = min($numberOfProcesses, $maximumNumberOfProcesses);
$this->storedData = [$cpuCores, count($files), count($jobs), $usedNumberOfProcesses, $decision];

return new Schedule($usedNumberOfProcesses, $jobs);
}

/**
* How many workers may run at once, and a human-readable account of why - which
* `diagnose` prints, because a user who thinks the number is wrong needs to see
* which input produced it.
*
* @return array{positive-int, string}
*/
private function resolveMaximumNumberOfProcesses(int $cpuCores): array
{
if ($this->maximumNumberOfProcesses !== self::AUTO) {
return [$this->maximumNumberOfProcesses, 'configured'];
}

return [
max(1, $cpuCores),
sprintf('auto, limited by %d usable CPU cores', $cpuCores),
];
}

public function print(Output $output): void
{
if ($this->storedData === null) {
return;
}

[$cpuCores, $filesCount, $jobsCount, $usedNumberOfProcesses] = $this->storedData;
[$cpuCores, $filesCount, $jobsCount, $usedNumberOfProcesses, $decision] = $this->storedData;

$output->writeLineFormatted('<info>Parallel processing scheduler:</info>');
$output->writeLineFormatted(sprintf('# of detected CPU cores: %d', $cpuCores));
$output->writeLineFormatted(sprintf('# of analysed files: %d', $filesCount));
$output->writeLineFormatted(sprintf('# of jobs: %d', $jobsCount));
$output->writeLineFormatted(sprintf('# of spawned processes: %d', $usedNumberOfProcesses));
$output->writeLineFormatted(sprintf('Process limit: %s', $decision));
$output->writeLineFormatted('');
}

Expand Down
34 changes: 31 additions & 3 deletions src/Process/CpuCoreCounter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,61 @@
use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use function min;

#[AutowiredService]
final class CpuCoreCounter
{

private ?int $count = null;

private ?int $detectedCount = null;

public function __construct(
#[AutowiredParameter(ref: '%parallel.loadLimit%')]
private ?float $loadLimit,
private SystemResources $systemResources,
)
{
}

/**
* Cores PHPStan may actually use: what the machine reports, capped by the CPU
* quota of the cgroup it runs in.
*/
public function getNumberOfCpuCores(): int
{
if ($this->count !== null) {
return $this->count;
}

$count = $this->getDetectedNumberOfCpuCores();

// fidry/cpu-core-counter has no cgroup finder, and its nproc-based default
// honours a cpuset affinity mask but not a CFS bandwidth quota, so inside a
// `docker run --cpus=2` container it reports the host's core count

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@theofidry is this something the cpu-core-counter should cover?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just found theofidry/cpu-core-counter#151

@AJenbo maybe you can contribute it upstream?

@AJenbo AJenbo Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I already planed to do so but since I'm also working on scaling based on memory, which requires the exact same probe, it won't actually provide any real benefits to have it in that package.

$quota = $this->systemResources->getCpuQuota();
if ($quota !== null) {
$count = min($count, $quota);
}

return $this->count = $count;
}

/** What the machine reports before any cgroup quota is applied. */
public function getDetectedNumberOfCpuCores(): int
{
if ($this->detectedCount !== null) {
return $this->detectedCount;
}

try {
$this->count = (new FidryCpuCoreCounter())->getAvailableForParallelisation(0, null, $this->loadLimit)->availableCpus;
$this->detectedCount = (new FidryCpuCoreCounter())->getAvailableForParallelisation(0, null, $this->loadLimit)->availableCpus;
} catch (NumberOfCpuCoreNotFound) {
$this->count = 1;
$this->detectedCount = 1;
}

return $this->count;
return $this->detectedCount;
}

}
Loading
Loading