Skip to content
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
98 changes: 97 additions & 1 deletion .github/workflows/phar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1175,10 +1175,106 @@
php -r 'exit(function_exists("pcntl_exec") && function_exists("pcntl_fork") ? 0 : 1);'

- name: "Run PHAR"
# No turbo binary sits next to this phar (the turbo-* jobs cover
# those), so ForkParallelChecker spawns the two workers phpstan.neon
# asks for. At -vvv each reports what it runs with: the OPcache
# setup its command line carries (see ProcessHelper) took effect.
run: |
php tmp/phpstan.phar --version
cd e2e/phar-run
php ../../tmp/phpstan.phar analyse -vvv
php ../../tmp/phpstan.phar analyse -vvv 2>&1 | tee output.txt
OPCACHE="$(php -r 'echo extension_loaded("Zend OPcache") ? "on" : "off";')"
grep -F "Spawned worker 1/2 checked in: turbo off, OPcache $OPCACHE, trusted types off" output.txt
grep -F "Spawned worker 2/2 checked in: turbo off, OPcache $OPCACHE, trusted types off" output.txt

phar-run-windows:
name: "Run PHAR on Windows, PHP ${{ matrix.php-version }}"
needs:
- compiler-tests
- turbo-compile-windows
runs-on: "windows-latest"
timeout-minutes: 20

defaults:
run:
shell: bash

strategy:
fail-fast: false
matrix:
include:
# below TurboExtensionSelector::MINIMUM_PHP_VERSION_ID there is no
# binary to find: the workers get the OPcache entries alone
- php-version: "8.2"
turbo: "off"
- php-version: "8.5"
turbo: "on"

steps:
- name: "Checkout"
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false

- name: "Install PHP"
uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
extensions: "opcache"

- name: "Download PHAR"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: phar-file
path: tmp

- name: "Download extension artifact"
if: matrix.turbo == 'on'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: "phpstan_turbo-windows-x86_64-php${{ matrix.php-version }}"
path: "tmp/turbo-artifact"

- name: "Place the extension next to the PHAR"
# the distribution layout TurboExtensionSelector looks for, see the
# turbo-artifact job
if: matrix.turbo == 'on'
env:
PHP_VERSION: ${{ matrix.php-version }}
run: |
mkdir -p tmp/turbo-ext/windows-x86_64
cp tmp/turbo-artifact/php_phpstan_turbo.dll "tmp/turbo-ext/windows-x86_64/phpstan_turbo-$PHP_VERSION.dll"

- name: "Diagnose"
# Without pcntl the main process cannot restart itself, so the
# workers are spawned and their command line is where the extension
# and OPcache come in - each worker with its own OPcache instance
# (see ProcessHelper).
env:
TURBO: ${{ matrix.turbo }}
run: |
cd e2e/phar-run
php ../../tmp/phpstan.phar diagnose --no-ansi 2>&1 | tee diagnose.txt
grep -F "Mechanism: spawn (react/child-process)" diagnose.txt
grep -F "opcache.enable_cli=1" diagnose.txt
grep -E "opcache\.cache_id=phpstan-[0-9]+-1" diagnose.txt
grep -F "phpstan.restarted=1" diagnose.txt
if [ "$TURBO" = on ]; then
grep -F "Turbo extension: enabled in worker processes" diagnose.txt
else
grep -F "Turbo extension: not loaded" diagnose.txt
fi

- name: "Run PHAR"
env:
TURBO: ${{ matrix.turbo }}
run: |
php tmp/phpstan.phar --version
cd e2e/phar-run
php ../../tmp/phpstan.phar analyse -vvv --no-ansi 2>&1 | tee output.txt
grep -F "Spawned worker 1/2 checked in: turbo $TURBO, OPcache on, trusted types $TURBO" output.txt
grep -F "Spawned worker 2/2 checked in: turbo $TURBO, OPcache on, trusted types $TURBO" output.txt

integration-tests:
if: github.event_name == 'pull_request'
Expand Down
6 changes: 6 additions & 0 deletions e2e/phar-run/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ parameters:
level: 8
paths:
- src
parallel:
# always two workers: the phar.yml phar-run jobs assert on what
# each one reports at -vvv (see ParallelAnalyser)
jobSize: 1
minimumNumberOfJobsPerProcess: 1
maximumNumberOfProcesses: 2
13 changes: 13 additions & 0 deletions e2e/phar-run/src/Bar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace PharRun;

final class Bar
{

public function doBar(string $s): int
{
return strlen($s);
}

}
13 changes: 13 additions & 0 deletions e2e/phar-run/src/Baz.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace PharRun;

final class Baz
{

public function doBaz(string $s): int
{
return strlen($s);
}

}
13 changes: 13 additions & 0 deletions e2e/phar-run/src/Qux.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace PharRun;

final class Qux
{

public function doQux(string $s): int
{
return strlen($s);
}

}
2 changes: 1 addition & 1 deletion src/Command/AnalyserRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public function runAnalyser(
if ($mainScript !== null && $schedule->getNumberOfProcesses() > 0) {
$loop = new StreamSelectLoop();
$result = null;
$promise = $this->parallelAnalyser->analyse($loop, $schedule, $allAnalysedFiles, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input, null);
$promise = $this->parallelAnalyser->analyse($loop, $schedule, $allAnalysedFiles, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input, null, $errorOutput);
$promise->then(static function (AnalyserResult $tmp) use (&$result): void {
$result = $tmp;
});
Expand Down
1 change: 1 addition & 0 deletions src/Command/FixerWorkerRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ private function runAnalyser(LoopInterface $loop, array $files, array $allAnalys
null,
$input,
$onFileAnalysisHandler,
null,
);
}

Expand Down
17 changes: 17 additions & 0 deletions src/Parallel/ForkParallelChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
use PHPStan\Command\Output;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Diagnose\DiagnoseExtension;
use PHPStan\Process\ProcessHelper;
use PHPStan\Turbo\TurboExtensionEnabler;
use PHPStan\Turbo\TurboProcessRestarter;
use function function_exists;
use function getmypid;
use function opcache_get_status;
use function sprintf;
use function str_starts_with;
use const PHP_OS_FAMILY;

/**
* Decides whether parallel analysis should fork workers via pcntl_fork()
Expand Down Expand Up @@ -62,6 +67,18 @@ public function print(Output $output): void

$output->writeLineFormatted('Mechanism: spawn (react/child-process)');
$output->writeLineFormatted(sprintf('Reason fork not used: %s', $reason));

// what a spawned worker's command line adds on top of the php.ini
// (see ProcessHelper); the extension path is on the turbo lines
$parentPid = getmypid();
$output->writeLineFormatted('Worker -d entries:');
foreach (ProcessHelper::resolveWorkerIniEntries(TurboProcessRestarter::getOpcacheArgs(), PHP_OS_FAMILY, $parentPid === false ? 0 : $parentPid, 1) as $iniEntry) {
$output->writeLineFormatted(sprintf(
' %s%s',
$iniEntry,
str_starts_with($iniEntry, 'opcache.cache_id=') ? ' (numbered per worker)' : '',
));
}
$output->writeLineFormatted('');
}

Expand Down
24 changes: 20 additions & 4 deletions src/Parallel/ParallelAnalyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Analyser\InternalError;
use PHPStan\Cache\ArenaCache;
use PHPStan\Command\CommandHelper;
use PHPStan\Command\Output;
use PHPStan\Dependency\RootExportedNode;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
Expand Down Expand Up @@ -67,6 +68,7 @@ public function __construct(
* @param string[] $allAnalysedFiles
* @param Closure(int, list<string>=): void|null $postFileCallback
* @param (callable(list<Error>, list<Error>, string[]): void)|null $onFileAnalysisHandler
* @param Output|null $errorOutput where spawned workers report what they run with at -vvv; null when nobody is listening
* @return PromiseInterface<AnalyserResult>
*/
public function analyse(
Expand All @@ -80,6 +82,7 @@ public function analyse(
?string $insteadOfFile,
InputInterface $input,
?callable $onFileAnalysisHandler,
?Output $errorOutput,
): PromiseInterface
{
$jobs = array_reverse($schedule->getJobs());
Expand Down Expand Up @@ -127,6 +130,8 @@ public function analyse(
/** @var Deferred<AnalyserResult> $deferred */
$deferred = new Deferred();

$useFork = $this->forkParallelChecker->isSupported();

$server = new TcpServer('127.0.0.1:0', $loop);
$this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$usedTraitDependencies, &$packageDependencies, &$exportedNodes, &$peakMemoryUsages, &$allProcessedFiles, $arenaName): void {
if ($arenaName !== null) {
Expand Down Expand Up @@ -166,13 +171,13 @@ public function analyse(
workerCount: count($peakMemoryUsages),
));
});
$server->on('connection', function (ConnectionInterface $connection) use (&$jobs, $arenaName, $expectedWorkerCount, &$helloCount): void {
$server->on('connection', function (ConnectionInterface $connection) use (&$jobs, $arenaName, $expectedWorkerCount, &$helloCount, $errorOutput, $useFork): void {
// phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly
$jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0;
// phpcs:enable
$decoder = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: $this->decoderBufferSize);
$encoder = new Encoder($connection, $jsonInvalidUtf8Ignore);
$decoder->on('data', function (array $data) use (&$jobs, $decoder, $encoder, $arenaName, $expectedWorkerCount, &$helloCount): void {
$decoder->on('data', function (array $data) use (&$jobs, $decoder, $encoder, $arenaName, $expectedWorkerCount, &$helloCount, $errorOutput, $useFork): void {
if ($data['action'] !== 'hello') {
return;
}
Expand All @@ -185,6 +190,19 @@ public function analyse(
if ($arenaName !== null && $helloCount === $expectedWorkerCount) {
ArenaCache::unlinkName();
}
if ($errorOutput !== null && $errorOutput->isVeryVerbose() && !$useFork) {
// the spawn command line asks for these (see ProcessHelper);
// this is whether they took effect. A forked worker inherits
// the main process and has nothing of its own to report.
$errorOutput->writeLineFormatted(sprintf(
'Spawned worker %d/%d checked in: turbo %s, OPcache %s, trusted types %s',
$helloCount,
$expectedWorkerCount,
($data['turbo'] ?? false) === true ? 'on' : 'off',
($data['opcache'] ?? false) === true ? 'on' : 'off',
($data['trustedTypes'] ?? false) === true ? 'on' : 'off',
));
}

$identifier = $data['identifier'];
$process = $this->processPool->getProcess($identifier);
Expand Down Expand Up @@ -217,8 +235,6 @@ public function analyse(
$this->processPool->quitAll();
};

$useFork = $this->forkParallelChecker->isSupported();

if ($useFork && $numberOfProcesses > 1) {
// Build the directory symbol indexes here, in the parent, so the
// children inherit them copy-on-write instead of each scanning the
Expand Down
33 changes: 32 additions & 1 deletion src/Parallel/WorkerRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Rules\Registry as RuleRegistry;
use PHPStan\Turbo\TurboExtensionEnabler;
use React\EventLoop\StreamSelectLoop;
use React\Socket\ConnectionInterface;
use React\Socket\TcpConnector;
Expand All @@ -30,7 +31,9 @@
use function array_unshift;
use function array_values;
use function defined;
use function function_exists;
use function memory_get_peak_usage;
use function opcache_get_status;
use function sprintf;

/**
Expand Down Expand Up @@ -60,6 +63,25 @@ public function __construct(
{
}

/**
* Whether this process compiles into an opcode cache. Off in a forked
* worker by design (see ForkedProcess), which is why ParallelAnalyser
* reports spawned workers only.
*/
private function isOpcacheEnabled(): bool
{
if (!function_exists('opcache_get_status')) {
return false;
}

$status = opcache_get_status(false);
if ($status === false) {
return false;
}

return ($status['opcache_enabled'] ?? false) === true;
}

/**
* @param string[] $analysedFiles the full analysed-files list (raw, before tmp-file substitution)
* @return int exit code (0 on success, 1 if a worker-communication error occurred)
Expand Down Expand Up @@ -100,7 +122,16 @@ public function run(
// phpcs:enable
$out = new Encoder($connection, $jsonInvalidUtf8Ignore);
$in = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: $this->decoderBufferSize);
$out->write(['action' => 'hello', 'identifier' => $identifier]);
$out->write([
'action' => 'hello',
'identifier' => $identifier,
// what this worker actually runs with, for the -vvv report of
// a spawning ParallelAnalyser: its command line asks for these
// (see ProcessHelper), this is whether they took effect
'turbo' => TurboExtensionEnabler::isActive(),
'opcache' => $this->isOpcacheEnabled(),
'trustedTypes' => TurboExtensionEnabler::isTrustingOwnTypes(),
]);
$this->runWorker($out, $in, $output, $analysedFiles, $tmpFile, $insteadOfFile, $errorCount);
});

Expand Down
Loading
Loading