Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Pool for much more efficient multithreaded parallelism #11219

Merged
merged 20 commits into from
Jan 30, 2025
Merged
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
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"composer-runtime-api": "^2",
"amphp/amp": "^3",
"amphp/byte-stream": "^2",
"amphp/parallel": "^2.3",
"composer/semver": "^1.4 || ^2.0 || ^3.0",
"composer/xdebug-handler": "^2.0 || ^3.0",
"dnoegel/php-xdg-base-dir": "^0.1.1",
Expand Down Expand Up @@ -75,7 +76,8 @@
},
"extra": {
"branch-alias": {
"dev-master": "6.x-dev",
"dev-master": "7.x-dev",
"dev-6.x": "6.x-dev",
"dev-5.x": "5.x-dev",
"dev-4.x": "4.x-dev",
"dev-3.x": "3.x-dev",
Expand Down
1 change: 1 addition & 0 deletions config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<xs:attribute name="disableSuppressAll" type="xs:boolean" default="false" />
<xs:attribute name="triggerErrorExits" type="TriggerErrorExitsType" default="default" />
<xs:attribute name="threads" type="xs:integer" />
<xs:attribute name="scanThreads" type="xs:integer" />
<xs:anyAttribute processContents="skip" />
</xs:complexType>

Expand Down
50 changes: 50 additions & 0 deletions docs/contributing/editing_callmaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,56 @@ optional parameter is postfixed with `=`, references with `&`/`&r_`/`&w_`/`&rw_`
(depending on the read/write meaning of the reference param) and
variadic args are prefixed with `...`.

Callmaps also support function aliases: aliases are very useful to specify that
a certain function behaves differently according to the parameter types.

For example, the following aliases are currently in use in the callmap to specify
that the `version_compare` function can be called with an `operator` parameter,
in which case it will return a boolean; otherwise it will return an integer.

```php
[
'version_compare' => [
0 => 'bool',
'version1' => 'string',
'version2' => 'string',
'operator' => '\'!=\'|\'<\'|\'<=\'|\'<>\'|\'=\'|\'==\'|\'>\'|\'>=\'|\'eq\'|\'ge\'|\'gt\'|\'le\'|\'lt\'|\'ne\'|null',
],
'version_compare\'1' => [
0 => 'int',
'version1' => 'string',
'version2' => 'string',
],
]
```

**Note**: the above example doesn't provide almost any useful information for type inference;
in fact, the real logic for return type inference is contained in the `VersionCompareReturnTypeProvider`.

The callmap is mainly useful when treating functions and methods as *callable values*, for example:

```php
<?php
function naive_version_compare(string $a, string $b, ?string $operator = null): int|bool {
return 0;
}

$a = ["1.0", "2.0"];

// OK
usort($a, "version_compare");

// InvalidArgument: Argument 2 of usort expects callable(string, string):int, but impure-callable(string, string, null|string=):(bool|int) provided
usort($a, "naive_version_compare");
```

The first usort succeeds, because psalm chooses the correct alias to use between the two provided in the callmap.
The second usort fails (equivalent to the non-split return type of `version_compare` inferred by reflection), because the return type is a union of the two possible signatures of version_compare.

When you have multifaceted functions like these, it's a very good idea to at least define a templated stub in `stubs/` for them, or a custom return type provider for even more complex logic, not representable with templates/conditional types/etc in a stub.

Also note that `bin/gen_callmap.php` has some validation logic which will re-add back removed parameters in overridden aliased callmaps: to avoid this, explicitly whitelist aliased functions by editing `assertParameter` in `bin/gen_callmap_utils.php`, and eventually `bin/gen_callmap.php` as needed.

## Delta file format

Delta files (named `CallMap_<PHP major version><PHP minor version>_delta.php`)
Expand Down
8 changes: 8 additions & 0 deletions docs/running_psalm/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ Allows you to hard-code a compressor for Psalm's cache. By default, Psalm uses `
```
Allows you to hard-code the number of threads Psalm will use (similar to `--threads` on the command line). This value will be used in place of detecting threads from the host machine, but will be overridden by using `--threads` or `--debug` (which sets threads to 1) on the command line

#### scanThreads
```xml
<psalm
scanThreads="[int]"
>
```
Allows you to hard-code the number of threads Psalm will use during the scan phase (as opposed to the analysis phase, controlled by the `threads` field) (similar to `--scan-threads` on the command line). This value will be used in place of detecting threads from the host machine, but will be overridden by using `--scan-threads` or `--debug` (which sets threads to 1) on the command line.

#### maxStringLength
```xml
<psalm
Expand Down
48 changes: 40 additions & 8 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@e9e270f363bd05ea3790890de3f04f6726ea5643">
<files psalm-version="6.x-dev@d3c7ca5430b3a75d8fab6439af1eefe119d12512">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset>
<code><![CDATA[$comment_block->tags['variablesfrom'][0]]]></code>
Expand Down Expand Up @@ -1106,7 +1106,6 @@
<code><![CDATA[!$paths_to_check]]></code>
<code><![CDATA[$baseline_file_path]]></code>
<code><![CDATA[$cache_directory]]></code>
<code><![CDATA[$config->threads]]></code>
<code><![CDATA[$find_references_to]]></code>
<code><![CDATA[empty($baselineFile)]]></code>
<code><![CDATA[getenv('PSALM_SHEPHERD')]]></code>
Expand Down Expand Up @@ -1368,19 +1367,49 @@
<code><![CDATA[$this->type_description]]></code>
</RiskyTruthyFalsyComparison>
</file>
<file src="src/Psalm/Internal/Fork/ForkContext.php">
<InternalClass>
<code><![CDATA[new ContextChannel($ipcChannel)]]></code>
<code><![CDATA[new ExitFailure($exception)]]></code>
<code><![CDATA[new ExitFailure($exception)]]></code>
<code><![CDATA[new ExitSuccess($returnValue instanceof Future ? $returnValue->await() : $returnValue)]]></code>
</InternalClass>
<InternalMethod>
<code><![CDATA[getResult]]></code>
<code><![CDATA[new ContextChannel($ipcChannel)]]></code>
<code><![CDATA[new ExitFailure($exception)]]></code>
<code><![CDATA[new ExitFailure($exception)]]></code>
<code><![CDATA[new ExitSuccess($returnValue instanceof Future ? $returnValue->await() : $returnValue)]]></code>
</InternalMethod>
<MixedAssignment>
<code><![CDATA[$callable]]></code>
<code><![CDATA[$returnValue]]></code>
</MixedAssignment>
<MixedFunctionCall>
<code><![CDATA[$callable(new ContextChannel($ipcChannel))]]></code>
</MixedFunctionCall>
<MixedReturnStatement>
<code><![CDATA[$data->getResult()]]></code>
</MixedReturnStatement>
<UnusedVariable>
<code><![CDATA[$argc]]></code>
</UnusedVariable>
</file>
<file src="src/Psalm/Internal/Fork/Pool.php">
<PossiblyFalseArgument>
<code><![CDATA[$buffer]]></code>
</PossiblyFalseArgument>
<RiskyTruthyFalsyComparison>
<code><![CDATA[!$sockets]]></code>
</RiskyTruthyFalsyComparison>
<ArgumentTypeCoercion>
<code><![CDATA[$this->childConnectTimeout]]></code>
</ArgumentTypeCoercion>
</file>
<file src="src/Psalm/Internal/Fork/PsalmRestarter.php">
<RiskyTruthyFalsyComparison>
<code><![CDATA[$this->tmpIni]]></code>
</RiskyTruthyFalsyComparison>
</file>
<file src="src/Psalm/Internal/Fork/ScannerTask.php">
<PossiblyUnusedMethod>
<code><![CDATA[__construct]]></code>
</PossiblyUnusedMethod>
</file>
<file src="src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php">
<RiskyTruthyFalsyComparison>
<code><![CDATA[empty($message)]]></code>
Expand Down Expand Up @@ -2006,6 +2035,9 @@
</RiskyTruthyFalsyComparison>
</file>
<file src="src/Psalm/IssueBuffer.php">
<PossiblyUnusedMethod>
<code><![CDATA[getServer]]></code>
</PossiblyUnusedMethod>
<RiskyTruthyFalsyComparison>
<code><![CDATA[!$report_options->output_path]]></code>
<code><![CDATA[$parent_issue_type]]></code>
Expand Down
15 changes: 11 additions & 4 deletions src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
use function libxml_clear_errors;
use function libxml_get_errors;
use function libxml_use_internal_errors;
use function max;
use function mkdir;
use function phpversion;
use function preg_match;
Expand Down Expand Up @@ -463,7 +464,10 @@ final class Config
*/
public array $internal_stubs = [];

/** @var ?int<1, max> */
public ?int $threads = null;
/** @var ?int<1, max> */
public ?int $scan_threads = null;

/**
* A list of php extensions supported by Psalm.
Expand Down Expand Up @@ -1379,7 +1383,12 @@ private static function fromXmlAndPaths(
}

if (isset($config_xml['threads'])) {
$config->threads = (int)$config_xml['threads'];
$config->threads = max(1, (int)$config_xml['threads']);
$config->scan_threads = $config->threads;
}

if (isset($config_xml['scanThreads'])) {
$config->scan_threads = max(1, (int)$config_xml['scanThreads']);
}

return $config;
Expand Down Expand Up @@ -2428,9 +2437,7 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P
// as they might be autoloadable once we require the autoloader below
$codebase->classlikes->forgetMissingClassLikes();

$this->include_collector->runAndCollect(
$this->requireAutoloader(...),
);
$this->include_collector->runAndCollect($this->requireAutoloader(...));
}

$this->collectPredefinedConstants();
Expand Down
48 changes: 8 additions & 40 deletions src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Psalm\Internal\Analyzer;

use Fidry\CpuCoreCounter\CpuCoreCounter;
use Fidry\CpuCoreCounter\NumberOfCpuCoreNotFound;
use InvalidArgumentException;
use Psalm\Codebase;
use Psalm\Config;
Expand Down Expand Up @@ -65,11 +63,9 @@
use function array_shift;
use function clearstatcache;
use function count;
use function defined;
use function dirname;
use function end;
use function explode;
use function extension_loaded;
use function file_exists;
use function fwrite;
use function implode;
Expand Down Expand Up @@ -203,7 +199,10 @@ public function __construct(
Providers $providers,
public ?ReportOptions $stdout_report_options = null,
public array $generated_report_options = [],
/** @var int<1, max> */
public int $threads = 1,
/** @var int<1, max> */
public int $scanThreads = 1,
?Progress $progress = null,
?Codebase $codebase = null,
) {
Expand Down Expand Up @@ -364,15 +363,6 @@ public function serverMode(LanguageServer $server): void
$this->file_reference_provider->loadReferenceCache();
$this->codebase->enterServerMode();

$cpu_count = self::getCpuCount();

// let's not go crazy
$usable_cpus = $cpu_count - 2;

if ($usable_cpus > 1) {
$this->threads = $usable_cpus;
}

$server->logInfo("Initializing: Initialize Plugins...");
$this->config->initializePlugins($this);

Expand Down Expand Up @@ -484,7 +474,7 @@ public function check(string $base_dir, bool $is_diff = false): void

$this->config->initializePlugins($this);

$this->codebase->scanFiles($this->threads);
$this->codebase->scanFiles($this->scanThreads);

$this->codebase->infer_types_from_usage = true;
} else {
Expand All @@ -508,7 +498,7 @@ public function check(string $base_dir, bool $is_diff = false): void

$this->config->initializePlugins($this);

$this->codebase->scanFiles($this->threads);
$this->codebase->scanFiles($this->scanThreads);
} else {
$diff_no_files = true;
}
Expand Down Expand Up @@ -885,7 +875,7 @@ public function checkDir(string $dir_name): void

$this->config->initializePlugins($this);

$this->codebase->scanFiles($this->threads);
$this->codebase->scanFiles($this->scanThreads);

$this->config->visitStubFiles($this->codebase, $this->progress);

Expand Down Expand Up @@ -995,7 +985,7 @@ public function checkFile(string $file_path): void

$this->config->initializePlugins($this);

$this->codebase->scanFiles($this->threads);
$this->codebase->scanFiles($this->scanThreads);

$this->config->visitStubFiles($this->codebase, $this->progress);

Expand Down Expand Up @@ -1039,7 +1029,7 @@ public function checkPaths(array $paths_to_check): void
$this->config->initializePlugins($this);


$this->codebase->scanFiles($this->threads);
$this->codebase->scanFiles($this->scanThreads);

$this->config->visitStubFiles($this->codebase, $this->progress);

Expand Down Expand Up @@ -1311,28 +1301,6 @@ public function getFunctionLikeAnalyzer(
return $function_analyzer;
}

/**
* Adapted from https://gist.github.com/divinity76/01ef9ca99c111565a72d3a8a6e42f7fb
* returns number of cpu cores
* Copyleft 2018, license: WTFPL
*
* @throws NumberOfCpuCoreNotFound
*/
public static function getCpuCount(): int
{
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
// No support desired for Windows at the moment
return 1;
}

if (!extension_loaded('pcntl')) {
// Psalm requires pcntl for multi-threads support
return 1;
}

return (new CpuCoreCounter())->getCount();
}

/**
* @return array<int, string>
* @psalm-pure
Expand Down
Loading
Loading