diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..a7c02ef
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,5 @@
+use nix --packages \
+ gnumake \
+ yamllint
+
+source_env_if_exists .envrc.local
diff --git a/.github/workflows/inspection.yaml b/.github/workflows/inspection.yaml
index b00d42e..1b0d070 100644
--- a/.github/workflows/inspection.yaml
+++ b/.github/workflows/inspection.yaml
@@ -27,17 +27,17 @@ jobs:
image: "macos-14"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Execute finders"
run: "php bin/execute.php"
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 40fe590..b9009d5 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -18,17 +18,17 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Install ComposerNormalize"
run: "phive install composer-normalize --trust-gpg-keys C00543248C87FB13"
@@ -54,17 +54,17 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Run Auto-Review tests"
run: "make autoreview"
@@ -82,16 +82,16 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Run the PHPUnit tests"
run: "make phpunit"
@@ -106,7 +106,7 @@ jobs:
- "7.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Remove incompatible dev dependencies"
run: |
@@ -115,13 +115,13 @@ jobs:
composer remove --dev webmozarts/strict-phpunit --no-update --no-install;
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Run the PHPUnit tests"
run: "vendor/bin/phpunit --configuration phpunit_legacy.xml.dist"
@@ -135,17 +135,17 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Install Infection"
run: "phive install infection --trust-gpg-keys C5095986493B4AA0"
@@ -171,17 +171,17 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Setup the expected output"
run: "mv e2e/expected-output-ubuntu e2e/expected-output"
@@ -198,17 +198,17 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Setup the expected output"
run: "mv e2e/expected-output-osx e2e/expected-output"
@@ -225,17 +225,17 @@ jobs:
- "8.3"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Setup the expected output"
run: "mv e2e/expected-output-windows e2e/expected-output"
@@ -252,17 +252,17 @@ jobs:
- "8.2"
steps:
- name: "Checkout"
- uses: "actions/checkout@v4.1.1"
+ uses: "actions/checkout@v4.1.7"
- name: "Set up PHP"
- uses: "shivammathur/setup-php@2.29.0"
+ uses: "shivammathur/setup-php@2.31.1"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
tools: "phive"
- name: "Install Composer dependencies"
- uses: "ramsey/composer-install@v2"
+ uses: "ramsey/composer-install@v3"
- name: "Setup the expected output"
run: "mv e2e/expected-output-ubuntu-restricted e2e/expected-output"
diff --git a/.gitignore b/.gitignore
index 353ee84..4bc36e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
+.envrc.local
/.build/
+/.direnv/
/composer.lock
/e2e/actual-output
/e2e/expected-output
diff --git a/.phive/phars.xml b/.phive/phars.xml
index e32840a..ceb6c74 100644
--- a/.phive/phars.xml
+++ b/.phive/phars.xml
@@ -1,6 +1,6 @@
-
-
-
+
+
+
diff --git a/README.md b/README.md
index 4816935..6b554d2 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,10 @@ use Fidry\CpuCoreCounter\Finder\DummyCpuCoreFinder;
$counter = new CpuCoreCounter();
+// For knowing the number of cores you can use for launching parallel processes:
+$counter->getAvailableForParallelisation()->availableCpus;
+
+// Get the number of CPU cores (by default it will use the logical cores count):
try {
$counter->getCount(); // e.g. 8
} catch (NumberOfCpuCoreNotFound) {
@@ -29,6 +33,10 @@ $counter = new CpuCoreCounter([
new DummyCpuCoreFinder(1), // Fallback value
]);
+// A type-safe alternative form:
+$counter->getCountWithFallback(1);
+
+// Note that the result is memoized.
$counter->getCount(); // e.g. 8
```
@@ -68,30 +76,46 @@ $cores = (new CpuCoreCounter($finders))->getCount();
`FinderRegistry` provides two helpful entries:
- `::getDefaultLogicalFinders()`: gives an ordered list of finders that will
- look for the _logical_ CPU cores count
+ look for the _logical_ CPU cores count.
- `::getDefaultPhysicalFinders()`: gives an ordered list of finders that will
- look for the _physical_ CPU cores count
+ look for the _physical_ CPU cores count.
-By default when using `CpuCoreCounter`, it will use the logical finders since
+By default, when using `CpuCoreCounter`, it will use the logical finders since
it is more likely what you are looking for and is what is used by PHP source to
build the PHP binary.
### Checks what finders find what on your system
-You have two commands available that provides insight about what the finders
+You have three scrips available that provides insight about what the finders
can find:
-```
-$ make diagnose # From this repository
-$ ./vendor/fidry/cpu-core-counter/bin/diagnose.php # From the library
+```shell
+# Checks what each given finder will find on your system with details about the
+# information it had.
+make diagnose # From this repository
+./vendor/fidry/cpu-core-counter/bin/diagnose.php # From the library
```
And:
+```shell
+# Execute all finders and display the result they found.
+make execute # From this repository
+./vendor/fidry/cpu-core-counter/bin/execute.php # From the library
```
-$ make execute # From this repository
-$ ./vendor/fidry/cpu-core-counter/bin/execute.php # From the library
-```
+
+
+### Debug the results found
+
+You have 3 methods available to help you find out what happened:
+
+1. If you are using the default configuration of finder registries, you can check
+ the previous section which will provide plenty of information.
+2. If what you are interested in is how many CPU cores were found, you can use
+ the `CpuCoreCounter::trace()` method.
+3. If what you are interested in is how the calculation of CPU cores available
+ for parallelisation was done, you can inspect the values of `ParallelisationResult`
+ returned by `CpuCoreCounter::getAvailableForParallelisation()`.
## Backward Compatibility Promise (BCP)
diff --git a/bin/trace.php b/bin/trace.php
new file mode 100755
index 0000000..adb52e2
--- /dev/null
+++ b/bin/trace.php
@@ -0,0 +1,32 @@
+#!/usr/bin/env php
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+use Fidry\CpuCoreCounter\CpuCoreCounter;
+use Fidry\CpuCoreCounter\Finder\FinderRegistry;
+
+require_once __DIR__.'/../vendor/autoload.php';
+
+$separator = str_repeat('–', 80);
+
+echo 'With all finders...'.PHP_EOL.PHP_EOL;
+echo (new CpuCoreCounter(FinderRegistry::getAllVariants()))->trace().PHP_EOL;
+echo $separator.PHP_EOL.PHP_EOL;
+
+echo 'Logical CPU cores finders...'.PHP_EOL.PHP_EOL;
+echo (new CpuCoreCounter(FinderRegistry::getDefaultLogicalFinders()))->trace().PHP_EOL;
+echo $separator.PHP_EOL.PHP_EOL;
+
+echo 'Physical CPU cores finders...'.PHP_EOL.PHP_EOL;
+echo (new CpuCoreCounter(FinderRegistry::getDefaultPhysicalFinders()))->trace().PHP_EOL;
+echo $separator.PHP_EOL.PHP_EOL;
diff --git a/infection.json5 b/infection.json5
index 36a0cf5..57864a2 100644
--- a/infection.json5
+++ b/infection.json5
@@ -23,23 +23,60 @@
"Fidry\\CpuCoreCounter\\CpuCoreCounter::getDefaultFinders"
]
},
+ "CastInt": {
+ "ignore": [
+ // This is a bug or case handled by strict types. Not sure why
+ // infection can't detect it.
+ "Fidry\\CpuCoreCounter\\Finder\\EnvVariableFinder::isPositiveInteger"
+ ]
+ },
"CastString": {
"ignore": [
- // I can't find a case in practice where this would happen
+ // I can't find a case in practice where this would happen.
"Fidry\\CpuCoreCounter\\Executor\\ProcOpenExecutor::execute"
]
},
+ "Coalesce": {
+ "ignore": [
+ // Cannot test this case.
+ "Fidry\\CpuCoreCounter\\CpuCoreCounter::getAvailableForParallelisation",
+ // Not interested in testing this case.
+ "Fidry\\CpuCoreCounter\\Finder\\ProcOpenBasedFinder::diagnose"
+ ]
+ },
"Continue_": false,
+ "DecrementInteger": {
+ "ignore": [
+ "Fidry\\CpuCoreCounter\\CpuCoreCounter::getAvailableForParallelisation"
+ ]
+ },
"FunctionCallRemoval": {
"ignore": [
- // I can't find a case in practice where this would happen
+ // I can't find a case in practice where this would happen.
"Fidry\\CpuCoreCounter\\Executor\\ProcOpenExecutor::execute"
]
},
+ "GreaterThan": {
+ "ignore": [
+ // This is an actual false positive.
+ "Fidry\\CpuCoreCounter\\CpuCoreCounter::getAvailableForParallelisation"
+ ]
+ },
+ "IncrementInteger": {
+ "ignore": [
+ "Fidry\\CpuCoreCounter\\CpuCoreCounter::getAvailableForParallelisation"
+ ]
+ },
+ "OneZeroFloat": {
+ "ignore": [
+ // Cannot test this case.
+ "Fidry\\CpuCoreCounter\\CpuCoreCounter::getAvailableForParallelisation"
+ ]
+ },
"PublicVisibility": false,
"TrueValue": {
"ignore": [
- // This is a case where the value does not matter
+ // This is a case where the value does not matter.
"Fidry\\CpuCoreCounter\\Finder\\LscpuPhysicalFinder::countCpuCores"
]
}
diff --git a/phpstan.src.neon b/phpstan.src.neon
index 83164c8..a35599e 100644
--- a/phpstan.src.neon
+++ b/phpstan.src.neon
@@ -1,5 +1,4 @@
parameters:
- checkMissingIterableValueType: false
inferPrivatePropertyTypeFromConstructor: true
level: max
@@ -8,3 +7,11 @@ parameters:
- src
tmpDir: .build/phpstan/src/
+
+ ignoreErrors:
+
+ - path: src/Finder/EnvVariableFinder.php
+ message: '#find\(\) should return int\<1\, max\>\|null but returns int\|null\.#'
+
+ - path: src/CpuCoreCounter.php
+ message: '#ParallelisationResult constructor expects int\<1\, max\>, int given\.#'
diff --git a/phpstan.tests.neon b/phpstan.tests.neon
index f145162..cdd6fde 100644
--- a/phpstan.tests.neon
+++ b/phpstan.tests.neon
@@ -1,5 +1,4 @@
parameters:
- checkMissingIterableValueType: false
inferPrivatePropertyTypeFromConstructor: true
level: 6
@@ -10,6 +9,7 @@ parameters:
tmpDir: .build/phpstan/tests/
ignoreErrors:
+ - identifier: missingType.iterableValue
# This is a sanity check
- path: tests/CpuCoreCounterTest.php
diff --git a/src/CpuCoreCounter.php b/src/CpuCoreCounter.php
index 098693b..8c8b55b 100644
--- a/src/CpuCoreCounter.php
+++ b/src/CpuCoreCounter.php
@@ -14,7 +14,14 @@
namespace Fidry\CpuCoreCounter;
use Fidry\CpuCoreCounter\Finder\CpuCoreFinder;
+use Fidry\CpuCoreCounter\Finder\EnvVariableFinder;
use Fidry\CpuCoreCounter\Finder\FinderRegistry;
+use InvalidArgumentException;
+use function implode;
+use function max;
+use function sprintf;
+use function sys_getloadavg;
+use const PHP_EOL;
final class CpuCoreCounter
{
@@ -36,6 +43,89 @@ public function __construct(?array $finders = null)
$this->finders = $finders ?? FinderRegistry::getDefaultLogicalFinders();
}
+ /**
+ * @param positive-int|0 $reservedCpus Number of CPUs to reserve. This is useful when you want
+ * to reserve some CPUs for other processes. If the main
+ * process is going to be busy still, you may want to set
+ * this value to 1.
+ * @param non-zero-int|null $countLimit The maximum number of CPUs to return. If not provided, it
+ * may look for a limit in the environment variables, e.g.
+ * KUBERNETES_CPU_LIMIT. If negative, the limit will be
+ * the total number of cores found minus the absolute value.
+ * For instance if the system has 10 cores and countLimit=-2,
+ * then the effective limit considered will be 8.
+ * @param float|null $loadLimit Element of [0., 1.]. Percentage representing the
+ * amount of cores that should be used among the available
+ * resources. For instance, if set to 0.7, it will use 70%
+ * of the available cores, i.e. if 1 core is reserved, 11
+ * cores are available and 5 are busy, it will use 70%
+ * of (11-1-5)=5 cores, so 3 cores. Set this parameter to null
+ * to skip this check. Beware that 1 does not mean "no limit",
+ * but 100% of the _available_ resources, i.e. with the
+ * previous example, it will return 5 cores. How busy is
+ * the system is determined by the system load average
+ * (see $systemLoadAverage).
+ * @param float|null $systemLoadAverage The system load average. If passed, it will use
+ * this information to limit the available cores based
+ * on the _available_ resources. For instance, if there
+ * is 10 cores but 3 are busy, then only 7 cores will
+ * be considered for further calculation. If set to
+ * `null`, it will use `sys_getloadavg()` to check the
+ * load of the system in the past minute. You can
+ * otherwise pass an arbitrary value. Should be a
+ * positive float.
+ *
+ * @see https://php.net/manual/en/function.sys-getloadavg.php
+ */
+ public function getAvailableForParallelisation(
+ int $reservedCpus = 0,
+ ?int $countLimit = null,
+ ?float $loadLimit = null,
+ ?float $systemLoadAverage = 0.
+ ): ParallelisationResult {
+ self::checkCountLimit($countLimit);
+ self::checkLoadLimit($loadLimit);
+ self::checkSystemLoadAverage($systemLoadAverage);
+
+ $totalCoreCount = $this->getCountWithFallback(1);
+ $availableCores = max(1, $totalCoreCount - $reservedCpus);
+
+ // Adjust available CPUs based on current load
+ if (null !== $loadLimit) {
+ $correctedSystemLoadAverage = null === $systemLoadAverage
+ ? sys_getloadavg()[0] ?? 0.
+ : $systemLoadAverage;
+
+ $availableCores = max(
+ 1,
+ $loadLimit * ($availableCores - $correctedSystemLoadAverage)
+ );
+ }
+
+ if (null === $countLimit) {
+ $correctedCountLimit = self::getKubernetesLimit();
+ } else {
+ $correctedCountLimit = $countLimit > 0
+ ? $countLimit
+ : max(1, $totalCoreCount + $countLimit);
+ }
+
+ if (null !== $correctedCountLimit && $availableCores > $correctedCountLimit) {
+ $availableCores = $correctedCountLimit;
+ }
+
+ return new ParallelisationResult(
+ $reservedCpus,
+ $countLimit,
+ $loadLimit,
+ $systemLoadAverage,
+ $correctedCountLimit,
+ $correctedSystemLoadAverage ?? $systemLoadAverage,
+ $totalCoreCount,
+ (int) $availableCores
+ );
+ }
+
/**
* @throws NumberOfCpuCoreNotFound
*
@@ -51,6 +141,48 @@ public function getCount(): int
return $this->count;
}
+ /**
+ * @param positive-int $fallback
+ *
+ * @return positive-int
+ */
+ public function getCountWithFallback(int $fallback): int
+ {
+ try {
+ return $this->getCount();
+ } catch (NumberOfCpuCoreNotFound $exception) {
+ return $fallback;
+ }
+ }
+
+ /**
+ * This method is mostly for debugging purposes.
+ */
+ public function trace(): string
+ {
+ $output = [];
+
+ foreach ($this->finders as $finder) {
+ $output[] = sprintf(
+ 'Executing the finder "%s":',
+ $finder->toString()
+ );
+ $output[] = $finder->diagnose();
+
+ $cores = $finder->find();
+
+ if (null !== $cores) {
+ $output[] = 'Result found: '.$cores;
+
+ break;
+ }
+
+ $output[] = '–––';
+ }
+
+ return implode(PHP_EOL, $output);
+ }
+
/**
* @throws NumberOfCpuCoreNotFound
*
@@ -86,4 +218,51 @@ public function getFinderAndCores(): array
throw NumberOfCpuCoreNotFound::create();
}
+
+ /**
+ * @return positive-int|null
+ */
+ public static function getKubernetesLimit(): ?int
+ {
+ $finder = new EnvVariableFinder('KUBERNETES_CPU_LIMIT');
+
+ return $finder->find();
+ }
+
+ private static function checkCountLimit(?int $countLimit): void
+ {
+ if (0 === $countLimit) {
+ throw new InvalidArgumentException(
+ 'The count limit must be a non zero integer. Got "0".'
+ );
+ }
+ }
+
+ private static function checkLoadLimit(?float $loadLimit): void
+ {
+ if (null === $loadLimit) {
+ return;
+ }
+
+ if ($loadLimit < 0. || $loadLimit > 1.) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'The load limit must be in the range [0., 1.], got "%s".',
+ $loadLimit
+ )
+ );
+ }
+ }
+
+ private static function checkSystemLoadAverage(?float $systemLoadAverage): void
+ {
+ if (null !== $systemLoadAverage && $systemLoadAverage < 0.) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'The system load average must be a positive float, got "%s".',
+ $systemLoadAverage
+ )
+ );
+ }
+ }
}
diff --git a/src/Finder/CpuInfoFinder.php b/src/Finder/CpuInfoFinder.php
index dea4c41..8013877 100644
--- a/src/Finder/CpuInfoFinder.php
+++ b/src/Finder/CpuInfoFinder.php
@@ -49,10 +49,12 @@ public function diagnose(): string
}
return sprintf(
- 'Found the file "%s" with the content:%s%s',
+ 'Found the file "%s" with the content:%s%s%sWill return "%s".',
self::CPU_INFO_PATH,
PHP_EOL,
- $cpuInfo
+ $cpuInfo,
+ PHP_EOL,
+ self::countCpuCores($cpuInfo)
);
}
diff --git a/src/Finder/EnvVariableFinder.php b/src/Finder/EnvVariableFinder.php
new file mode 100644
index 0000000..fa23278
--- /dev/null
+++ b/src/Finder/EnvVariableFinder.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Fidry\CpuCoreCounter\Finder;
+
+use function getenv;
+use function preg_match;
+use function sprintf;
+use function var_export;
+
+final class EnvVariableFinder implements CpuCoreFinder
+{
+ /** @var string */
+ private $environmentVariableName;
+
+ public function __construct(string $environmentVariableName)
+ {
+ $this->environmentVariableName = $environmentVariableName;
+ }
+
+ public function diagnose(): string
+ {
+ $value = getenv($this->environmentVariableName);
+
+ return sprintf(
+ 'parse(getenv(%s)=%s)=%s',
+ $this->environmentVariableName,
+ var_export($value, true),
+ self::isPositiveInteger($value) ? $value : 'null'
+ );
+ }
+
+ public function find(): ?int
+ {
+ $value = getenv($this->environmentVariableName);
+
+ return self::isPositiveInteger($value)
+ ? (int) $value
+ : null;
+ }
+
+ public function toString(): string
+ {
+ return sprintf(
+ 'getenv(%s)',
+ $this->environmentVariableName
+ );
+ }
+
+ /**
+ * @param string|false $value
+ */
+ private static function isPositiveInteger($value): bool
+ {
+ return false !== $value
+ && 1 === preg_match('/^\d+$/', $value)
+ && (int) $value > 0;
+ }
+}
diff --git a/src/Finder/NProcFinder.php b/src/Finder/NProcFinder.php
index 60a8ab7..c0f7a6f 100644
--- a/src/Finder/NProcFinder.php
+++ b/src/Finder/NProcFinder.php
@@ -30,10 +30,13 @@ final class NProcFinder extends ProcOpenBasedFinder
private $all;
/**
- * @param bool $all If disabled will give the number of cores available for the current process only.
+ * @param bool $all If disabled will give the number of cores available for the current process
+ * only. This is disabled by default as it is known to be "buggy" on virtual
+ * environments as the virtualization tool, e.g. VMWare, might over-commit
+ * resources by default.
*/
public function __construct(
- bool $all = true,
+ bool $all = false,
?ProcessExecutor $executor = null
) {
parent::__construct($executor);
diff --git a/src/Finder/ProcOpenBasedFinder.php b/src/Finder/ProcOpenBasedFinder.php
index 793ec64..4d51f89 100644
--- a/src/Finder/ProcOpenBasedFinder.php
+++ b/src/Finder/ProcOpenBasedFinder.php
@@ -56,16 +56,19 @@ public function diagnose(): string
return $failed
? sprintf(
- 'Executed the command "%s" which wrote the following output to the STDERR:%s%s',
+ 'Executed the command "%s" which wrote the following output to the STDERR:%s%s%sWill return "null".',
$command,
PHP_EOL,
- $stderr
+ $stderr,
+ PHP_EOL
)
: sprintf(
- 'Executed the command "%s" and got the following (STDOUT) output:%s%s',
+ 'Executed the command "%s" and got the following (STDOUT) output:%s%s%sWill return "%s".',
$command,
PHP_EOL,
- $stdout
+ $stdout,
+ PHP_EOL,
+ $this->countCpuCores($stdout) ?? 'null'
);
}
diff --git a/src/ParallelisationResult.php b/src/ParallelisationResult.php
new file mode 100644
index 0000000..1f29434
--- /dev/null
+++ b/src/ParallelisationResult.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Fidry\CpuCoreCounter;
+
+/**
+ * @readonly
+ */
+final class ParallelisationResult
+{
+ /**
+ * @var positive-int|0
+ */
+ public $passedReservedCpus;
+
+ /**
+ * @var non-zero-int|null
+ */
+ public $passedCountLimit;
+
+ /**
+ * @var float|null
+ */
+ public $passedLoadLimit;
+
+ /**
+ * @var float|null
+ */
+ public $passedSystemLoadAverage;
+
+ /**
+ * @var non-zero-int|null
+ */
+ public $correctedCountLimit;
+
+ /**
+ * @var float|null
+ */
+ public $correctedSystemLoadAverage;
+
+ /**
+ * @var positive-int
+ */
+ public $totalCoresCount;
+
+ /**
+ * @var positive-int
+ */
+ public $availableCpus;
+
+ /**
+ * @param positive-int|0 $passedReservedCpus
+ * @param non-zero-int|null $passedCountLimit
+ * @param non-zero-int|null $correctedCountLimit
+ * @param positive-int $totalCoresCount
+ * @param positive-int $availableCpus
+ */
+ public function __construct(
+ int $passedReservedCpus,
+ ?int $passedCountLimit,
+ ?float $passedLoadLimit,
+ ?float $passedSystemLoadAverage,
+ ?int $correctedCountLimit,
+ ?float $correctedSystemLoadAverage,
+ int $totalCoresCount,
+ int $availableCpus
+ ) {
+ $this->passedReservedCpus = $passedReservedCpus;
+ $this->passedCountLimit = $passedCountLimit;
+ $this->passedLoadLimit = $passedLoadLimit;
+ $this->passedSystemLoadAverage = $passedSystemLoadAverage;
+ $this->correctedCountLimit = $correctedCountLimit;
+ $this->correctedSystemLoadAverage = $correctedSystemLoadAverage;
+ $this->totalCoresCount = $totalCoresCount;
+ $this->availableCpus = $availableCpus;
+ }
+}
diff --git a/tests/AvailableCpuCoresScenario.php b/tests/AvailableCpuCoresScenario.php
new file mode 100644
index 0000000..0aa8fc9
--- /dev/null
+++ b/tests/AvailableCpuCoresScenario.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Fidry\CpuCoreCounter\Test;
+
+use Fidry\CpuCoreCounter\Finder\CpuCoreFinder;
+use Fidry\CpuCoreCounter\Finder\DummyCpuCoreFinder;
+
+/**
+ * @internal
+ * @readonly
+ */
+final class AvailableCpuCoresScenario
+{
+ /** @var list */
+ public $finders;
+ /** @var array */
+ public $environmentVariables;
+ /** @var positive-int|0 */
+ public $reservedCpus;
+ /** @var non-zero-int|null */
+ public $countLimit;
+ /** @var float|null */
+ public $loadLimit;
+ /** @var float|null */
+ public $systemLoadAverage;
+ /** @var positive-int */
+ public $expected;
+
+ /**
+ * @param list $finders
+ * @param array $environmentVariables
+ * @param positive-int|0 $reservedCpus
+ * @param non-zero-int|null $countLimit
+ * @param positive-int $expected
+ */
+ public function __construct(
+ array $finders,
+ array $environmentVariables,
+ int $reservedCpus,
+ ?int $countLimit,
+ ?float $loadLimit,
+ ?float $systemLoadAverage,
+ int $expected
+ ) {
+ $this->finders = $finders;
+ $this->environmentVariables = $environmentVariables;
+ $this->reservedCpus = $reservedCpus;
+ $this->countLimit = $countLimit;
+ $this->loadLimit = $loadLimit;
+ $this->systemLoadAverage = $systemLoadAverage;
+ $this->expected = $expected;
+ }
+
+ /**
+ * @param positive-int|null $coresCountFound
+ * @param array $environmentVariables
+ * @param positive-int|0|null $reservedCpus
+ * @param non-zero-int|null $countLimit
+ * @param positive-int $expected
+ *
+ * @return array{self}
+ */
+ public static function create(
+ ?int $coresCountFound,
+ array $environmentVariables,
+ ?int $reservedCpus,
+ ?int $countLimit,
+ ?float $loadLimit,
+ ?float $systemLoadAverage,
+ int $expected
+ ): array {
+ $finders = null === $coresCountFound
+ ? []
+ : [new DummyCpuCoreFinder($coresCountFound)];
+
+ return [
+ new self(
+ $finders,
+ $environmentVariables,
+ $reservedCpus ?? 0,
+ $countLimit,
+ $loadLimit,
+ $systemLoadAverage ?? 0.,
+ $expected
+ ),
+ ];
+ }
+}
diff --git a/tests/CpuCoreCounterTest.php b/tests/CpuCoreCounterTest.php
index cede7c1..f75d02f 100644
--- a/tests/CpuCoreCounterTest.php
+++ b/tests/CpuCoreCounterTest.php
@@ -13,6 +13,7 @@
namespace Fidry\CpuCoreCounter\Test;
+use Closure;
use Exception;
use Fidry\CpuCoreCounter\CpuCoreCounter;
use Fidry\CpuCoreCounter\Finder\CpuCoreFinder;
@@ -22,6 +23,7 @@
use PHPUnit\Framework\TestCase;
use function get_class;
use function is_array;
+use function sprintf;
/**
* @covers \Fidry\CpuCoreCounter\CpuCoreCounter
@@ -30,6 +32,21 @@
*/
final class CpuCoreCounterTest extends TestCase
{
+ /**
+ * @var null|Closure(): void
+ */
+ private $cleanupEnvironmentVariables;
+
+ protected function tearDown(): void
+ {
+ $cleanupEnvironmentVariables = $this->cleanupEnvironmentVariables;
+
+ if (null !== $cleanupEnvironmentVariables) {
+ ($cleanupEnvironmentVariables)();
+ $this->cleanupEnvironmentVariables = null;
+ }
+ }
+
public function test_it_can_get_the_number_of_cpu_cores(): void
{
$counter = new CpuCoreCounter();
@@ -150,4 +167,420 @@ public static function cpuCoreFinderProvider(): iterable
];
})();
}
+
+ /**
+ * @dataProvider availableCpuCoreProvider
+ */
+ public function test_it_can_get_the_number_of_available_cpu_cores_for_parallelisation(AvailableCpuCoresScenario $scenario): void
+ {
+ $this->setUpEnvironmentVariables($scenario->environmentVariables);
+
+ $counter = new CpuCoreCounter($scenario->finders);
+
+ $actual = $counter->getAvailableForParallelisation(
+ $scenario->reservedCpus,
+ $scenario->countLimit,
+ $scenario->loadLimit,
+ $scenario->systemLoadAverage
+ );
+
+ self::assertSame($scenario->expected, $actual->availableCpus);
+ }
+
+ public static function availableCpuCoreProvider(): iterable
+ {
+ yield 'no finder' => AvailableCpuCoresScenario::create(
+ null,
+ [],
+ 1,
+ null,
+ null,
+ null,
+ 1
+ );
+
+ yield 'no finder, multiple CPUs reserved' => AvailableCpuCoresScenario::create(
+ null,
+ [],
+ 3,
+ null,
+ null,
+ null,
+ 1
+ );
+
+ yield 'CPU count found: kubernetes limit set and lower than the count found' => AvailableCpuCoresScenario::create(
+ 5,
+ ['KUBERNETES_CPU_LIMIT' => 2],
+ 1,
+ null,
+ null,
+ null,
+ 2
+ );
+
+ yield 'CPU count found: kubernetes limit set and higher than the count found' => AvailableCpuCoresScenario::create(
+ 5,
+ ['KUBERNETES_CPU_LIMIT' => 8],
+ 1,
+ null,
+ null,
+ null,
+ 4
+ );
+
+ yield 'CPU count found: kubernetes limit set and equal to the count found' => AvailableCpuCoresScenario::create(
+ 5,
+ ['KUBERNETES_CPU_LIMIT' => 5],
+ 1,
+ null,
+ null,
+ null,
+ 4
+ );
+
+ yield 'CPU count found: kubernetes limit set and equal to the count found after reserved CPUs' => AvailableCpuCoresScenario::create(
+ 5,
+ ['KUBERNETES_CPU_LIMIT' => 4],
+ 1,
+ null,
+ null,
+ null,
+ 4
+ );
+
+ yield 'CPU count found: kubernetes limit set and limit set' => AvailableCpuCoresScenario::create(
+ 5,
+ ['KUBERNETES_CPU_LIMIT' => 2],
+ 1,
+ 3,
+ null,
+ null,
+ 3
+ );
+
+ yield 'CPU count found: by default it reserves no CPU' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ null,
+ null,
+ null,
+ null,
+ 5
+ );
+
+ yield 'CPU count found higher than the count limit passed' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ 1,
+ 3,
+ null,
+ null,
+ 3
+ );
+
+ yield 'CPU count found, negative limit passed' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ 0,
+ -2,
+ null,
+ null,
+ 3
+ );
+
+ yield 'CPU count found, negative limit beyond available resources' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ 0,
+ -10,
+ null,
+ null,
+ 1
+ );
+
+ yield 'CPU count found, with reserved CPU, negative limit passed' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ 1,
+ -2,
+ null,
+ null,
+ 3
+ );
+
+ yield 'CPU count found, multiple CPUs reserved' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ 2,
+ null,
+ null,
+ null,
+ 3
+ );
+
+ yield 'CPU count found, all CPUs reserved' => AvailableCpuCoresScenario::create(
+ 5,
+ [],
+ 5,
+ null,
+ null,
+ null,
+ 1
+ );
+
+ yield 'CPU count found, over half the cores are used and no limit is set' => AvailableCpuCoresScenario::create(
+ 11,
+ [],
+ 1,
+ null,
+ null,
+ 6.,
+ 10
+ );
+
+ yield 'CPU count found, over half the cores are used and a limit is set' => AvailableCpuCoresScenario::create(
+ 11,
+ [],
+ 1,
+ null,
+ 1.,
+ 6.,
+ 4
+ );
+
+ yield 'CPU count found, the CPUs are overloaded' => AvailableCpuCoresScenario::create(
+ 11,
+ [],
+ 1,
+ null,
+ .9,
+ 9.5,
+ 1
+ );
+
+ yield 'CPU count found, the load limit is set, but there is several CPUs available still' => AvailableCpuCoresScenario::create(
+ 11,
+ [],
+ 1,
+ null,
+ .5,
+ 6.,
+ 2
+ );
+
+ yield 'CPU count found, the CPUs are at completely overloaded' => AvailableCpuCoresScenario::create(
+ 11,
+ [],
+ 1,
+ null,
+ .5,
+ 11.,
+ 1
+ );
+
+ yield 'CPU count found, the CPUs are overloaded but no load limit per CPU' => AvailableCpuCoresScenario::create(
+ 11,
+ [],
+ 1,
+ null,
+ null,
+ 9.5,
+ 10
+ );
+
+ yield 'it rounds the available cores to the lower int (less than half)' => AvailableCpuCoresScenario::create(
+ 32,
+ [],
+ 0,
+ null,
+ .1,
+ 0.,
+ 3
+ );
+
+ yield 'it rounds the available cores to the lower int (perfect half)' => AvailableCpuCoresScenario::create(
+ 7,
+ [],
+ 0,
+ null,
+ .5,
+ 0.,
+ 3
+ );
+
+ yield 'it rounds the available cores to the lower int (more than half)' => AvailableCpuCoresScenario::create(
+ 36,
+ [],
+ 0,
+ null,
+ .1,
+ 0.,
+ 3
+ );
+ }
+
+ /**
+ * @dataProvider countLimitProvider
+ */
+ public function test_it_does_not_accept_invalid_count_limit(
+ int $countLimit,
+ ?string $expectedExceptionMessage
+ ): void {
+ $cpuCoreCounter = new CpuCoreCounter();
+
+ if (null !== $expectedExceptionMessage) {
+ $this->expectExceptionMessage($expectedExceptionMessage);
+ }
+
+ $cpuCoreCounter->getAvailableForParallelisation(
+ 1,
+ $countLimit
+ );
+
+ if (null === $expectedExceptionMessage) {
+ $this->addToAssertionCount(1);
+ }
+ }
+
+ public static function countLimitProvider(): iterable
+ {
+ yield 'below limit' => [
+ -2,
+ null,
+ ];
+
+ yield 'within the limit (lower)' => [
+ -1,
+ null,
+ ];
+
+ yield 'invalid limit' => [
+ 0,
+ 'The count limit must be a non zero integer. Got "0".',
+ ];
+
+ yield 'within the limit (upper)' => [
+ 1,
+ null,
+ ];
+
+ yield 'above limit' => [
+ 2,
+ null,
+ ];
+ }
+
+ /**
+ * @dataProvider loadLimitProvider
+ */
+ public function test_it_does_not_accept_invalid_load_limit(
+ float $loadLimit,
+ ?string $expectedExceptionMessage
+ ): void {
+ $cpuCoreCounter = new CpuCoreCounter();
+
+ if (null !== $expectedExceptionMessage) {
+ $this->expectExceptionMessage($expectedExceptionMessage);
+ }
+
+ $cpuCoreCounter->getAvailableForParallelisation(
+ 1,
+ null,
+ $loadLimit
+ );
+
+ if (null === $expectedExceptionMessage) {
+ $this->addToAssertionCount(1);
+ }
+ }
+
+ public static function loadLimitProvider(): iterable
+ {
+ yield 'below limit' => [
+ -0.001,
+ 'The load limit must be in the range [0., 1.], got "-0.001".',
+ ];
+
+ yield 'within the limit (min)' => [
+ 0.,
+ null,
+ ];
+
+ yield 'within the limit (max)' => [
+ 1.,
+ null,
+ ];
+
+ yield 'above limit' => [
+ 1.001,
+ 'The load limit must be in the range [0., 1.], got "1.001".',
+ ];
+ }
+
+ /**
+ * @dataProvider systemLoadAverageProvider
+ */
+ public function test_it_does_not_accept_invalid_system_load_average(
+ float $systemLoadAverage,
+ ?string $expectedExceptionMessage
+ ): void {
+ $cpuCoreCounter = new CpuCoreCounter();
+
+ if (null !== $expectedExceptionMessage) {
+ $this->expectExceptionMessage($expectedExceptionMessage);
+ }
+
+ $cpuCoreCounter->getAvailableForParallelisation(
+ 1,
+ null,
+ null,
+ $systemLoadAverage
+ );
+
+ if (null === $expectedExceptionMessage) {
+ $this->addToAssertionCount(1);
+ }
+ }
+
+ public static function systemLoadAverageProvider(): iterable
+ {
+ yield 'below limit' => [
+ -0.001,
+ 'The system load average must be a positive float, got "-0.001".',
+ ];
+
+ yield 'within the limit' => [
+ 0.,
+ null,
+ ];
+ }
+
+ /**
+ * @param array $environmentVariables
+ */
+ private function setUpEnvironmentVariables(array $environmentVariables): void
+ {
+ $cleanupCalls = [];
+
+ foreach ($environmentVariables as $environmentName => $environmentValue) {
+ putenv(
+ sprintf(
+ '%s=%s',
+ $environmentName,
+ $environmentValue
+ )
+ );
+
+ $cleanupCalls[] = static function () use ($environmentName): void {
+ putenv($environmentName);
+ };
+ }
+
+ $this->cleanupEnvironmentVariables = static function () use ($cleanupCalls): void {
+ foreach ($cleanupCalls as $cleanupCall) {
+ $cleanupCall();
+ }
+ };
+ }
}
diff --git a/tests/Finder/EnvVariableFinderTest.php b/tests/Finder/EnvVariableFinderTest.php
new file mode 100644
index 0000000..5638cde
--- /dev/null
+++ b/tests/Finder/EnvVariableFinderTest.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Fidry\CpuCoreCounter\Test\Finder;
+
+use Fidry\CpuCoreCounter\Finder\EnvVariableFinder;
+use PHPUnit\Framework\TestCase;
+use function sprintf;
+
+/**
+ * @covers \Fidry\CpuCoreCounter\Finder\EnvVariableFinder
+ *
+ * @internal
+ */
+final class EnvVariableFinderTest extends TestCase
+{
+ protected function tearDown(): void
+ {
+ putenv('CI_CPU_LIMIT');
+ }
+
+ public function test_it_can_describe_itself(): void
+ {
+ $finder = new EnvVariableFinder('CI_CPU_LIMIT');
+
+ self::assertSame(
+ 'getenv(CI_CPU_LIMIT)',
+ $finder->toString()
+ );
+ }
+
+ /**
+ * @dataProvider envProvider
+ */
+ public function test_it_tries_to_get_the_number_of_cores(
+ ?string $envValue,
+ ?int $expected
+ ): void {
+ $finder = new EnvVariableFinder('CI_CPU_LIMIT');
+
+ if (null !== $envValue) {
+ putenv(sprintf('CI_CPU_LIMIT=%s', $envValue));
+ }
+
+ self::assertSame($expected, $finder->find());
+ }
+
+ public static function envProvider(): iterable
+ {
+ yield 'int value' => [
+ '18',
+ 18,
+ ];
+
+ yield 'zero' => [
+ '0',
+ null,
+ ];
+
+ yield 'negative int value' => [
+ '-3',
+ null,
+ ];
+
+ yield 'no value' => [
+ '',
+ null,
+ ];
+
+ yield 'no environment variable' => [
+ null,
+ null,
+ ];
+
+ yield 'string value' => [
+ 'something',
+ null,
+ ];
+
+ yield 'numeric value' => [
+ '18.3',
+ null,
+ ];
+
+ yield 'int value in string' => [
+ '"something 18"',
+ null,
+ ];
+ }
+}
diff --git a/tests/Finder/NProcFinderTest.php b/tests/Finder/NProcFinderTest.php
index 61086ce..2b84e04 100644
--- a/tests/Finder/NProcFinderTest.php
+++ b/tests/Finder/NProcFinderTest.php
@@ -53,26 +53,26 @@ public function test_it_can_describe_itself(CpuCoreFinder $finder, string $expec
public static function finderProvider(): iterable
{
- yield [
- new NProcFinder(true),
+ yield 'default constructor parameters' => [
+ new NProcFinder(),
sprintf(
- '%s(all=true)',
+ '%s(all=false)',
FinderShortClassName::get(new NProcFinder())
)
];
- yield [
- new NProcFinder(),
+ yield 'without all' => [
+ new NProcFinder(false),
sprintf(
- '%s(all=true)',
+ '%s(all=false)',
FinderShortClassName::get(new NProcFinder())
)
];
- yield [
- new NProcFinder(false),
+ yield 'with all' => [
+ new NProcFinder(true),
sprintf(
- '%s(all=false)',
+ '%s(all=true)',
FinderShortClassName::get(new NProcFinder())
)
];
diff --git a/tests/Finder/ProcOpenBasedFinderTestCase.php b/tests/Finder/ProcOpenBasedFinderTestCase.php
index a0a79ba..0801708 100644
--- a/tests/Finder/ProcOpenBasedFinderTestCase.php
+++ b/tests/Finder/ProcOpenBasedFinderTestCase.php
@@ -63,8 +63,8 @@ public function test_it_can_do_a_diagnosis(
public static function diagnosisProvider(): iterable
{
- $stdoutResultRegex = '/^Executed the command ".*" and got the following \(STDOUT\) output:\nsmth in stdout$/';
- $stderrResultRegex = '/^Executed the command ".*" which wrote the following output to the STDERR:\nsmth in stderr$/';
+ $stdoutResultRegex = '/^Executed the command ".*" and got the following \(STDOUT\) output:\nsmth in stdout\nWill return "(null|\d)"\.$/';
+ $stderrResultRegex = '/^Executed the command ".*" which wrote the following output to the STDERR:\nsmth in stderr\nWill return "(null|\d)"\.$/';
yield 'could not execute command' => [
null,