Skip to content

Commit a915bdc

Browse files
committed
PHPStan Pro: analyse the files changed while an analysis runs once it ends
Files were not watched during an analysis, so an edit made while it ran was noticed only after it ended, by the next poll. They are watched during an analysis now, and the code shown follows them right away. Once they settle, the analysis in progress is left to finish - what it saves to the result cache makes the next one cheaper - and one more starts when it ends, for everything that changed meanwhile. One worker at a time writes the result cache. Stopping the analysis in progress instead was tried, on the Laravel framework with edits overlapping analyses: about the same time until the final result, but 35-40 % more CPU, because each stopped analysis is thrown away. Only while the API of a widely used class kept changing was stopping faster, by 3 s of 14 s. Cancelling the worker never stopped it anyway - terminating the process terminated the shell running its command - and is removed. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a533-a2c5-7581-b58e-fb459c136c67
1 parent 0f02c24 commit a915bdc

3 files changed

Lines changed: 22 additions & 52 deletions

File tree

‎src/Command/FixerApplication.php‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use PHPStan\Internal\DirectoryCreatorException;
2525
use PHPStan\Internal\HttpClientFactory;
2626
use PHPStan\PhpDoc\StubFilesProvider;
27-
use PHPStan\Process\ProcessCanceledException;
2827
use PHPStan\Process\ProcessCrashedException;
2928
use PHPStan\Process\ProcessHelper;
3029
use PHPStan\Process\SpawnedProcessPromise;
@@ -73,6 +72,9 @@ final class FixerApplication
7372
/** @var PromiseInterface<string>|null */
7473
private PromiseInterface|null $processInProgress = null;
7574

75+
/** Files changed during the analysis in progress, another one starts once it ends. */
76+
private bool $analysisRequested = false;
77+
7678
private bool $fileMonitorActive = true;
7779

7880
/** @var string[] watched files besides the analysed ones and the config files */
@@ -188,9 +190,11 @@ public function run(
188190
'paths' => $changes->getChangedFiles(),
189191
]]);
190192
}, function () use ($loop, $mainScript, $projectConfigFile, $input, $encoder): void {
193+
// The analysis in progress is not stopped: what it saves to the result cache makes the next
194+
// one cheaper, and that one starts once it ends, for everything that changed meanwhile.
191195
if ($this->processInProgress !== null) {
192-
$this->processInProgress->cancel();
193-
$this->processInProgress = null;
196+
$this->analysisRequested = true;
197+
return;
194198
}
195199

196200
$this->analyse(
@@ -424,10 +428,6 @@ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCal
424428
$loop->addTimer($interval, $callback);
425429
return;
426430
}
427-
if ($this->processInProgress !== null) {
428-
$loop->addTimer($interval, $callback);
429-
return;
430-
}
431431
$changes = $fileMonitor->getChanges();
432432
$now = microtime(true);
433433

@@ -506,17 +506,11 @@ private function analyse(
506506
));
507507
$this->processInProgress = $process->run();
508508

509-
$this->processInProgress->then(function () use ($server): void {
510-
$this->processInProgress = null;
509+
$this->processInProgress->then(static function () use ($server): void {
511510
$server->close();
512-
}, function (Throwable $e) use ($server, $phpstanFixerEncoder): void {
513-
$this->processInProgress = null;
511+
}, static function (Throwable $e) use ($server, $phpstanFixerEncoder): void {
514512
$server->close();
515513

516-
if ($e instanceof ProcessCanceledException) {
517-
return;
518-
}
519-
520514
if ($e instanceof ProcessCrashedException) {
521515
$message = 'Analysis crashed';
522516
$traceAsString = $e->getMessage();
@@ -535,6 +529,14 @@ private function analyse(
535529
shouldReportBug: false,
536530
)],
537531
]]);
532+
})->finally(function () use ($loop, $mainScript, $projectConfigFile, $input, $phpstanFixerEncoder): void {
533+
$this->processInProgress = null;
534+
if (!$this->analysisRequested) {
535+
return;
536+
}
537+
538+
$this->analysisRequested = false;
539+
$this->analyse($loop, $mainScript, $projectConfigFile, $input, $phpstanFixerEncoder);
538540
});
539541
}
540542

‎src/Process/ProcessCanceledException.php‎

Lines changed: 0 additions & 10 deletions
This file was deleted.

‎src/Process/SpawnedProcessPromise.php‎

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,17 @@
1414

1515
/**
1616
* A freshly spawned PHP process (react/child-process) as a promise that resolves
17-
* with its output on success and rejects with ProcessCrashedException or
18-
* ProcessCanceledException otherwise.
17+
* with its output on success and rejects with ProcessCrashedException otherwise.
1918
*/
2019
final class SpawnedProcessPromise
2120
{
2221

2322
/** @var Deferred<string> */
2423
private Deferred $deferred;
2524

26-
private ?Process $process = null;
27-
28-
private bool $canceled = false;
29-
3025
public function __construct(private LoopInterface $loop, private string $command)
3126
{
32-
$this->deferred = new Deferred(function (): void {
33-
$this->cancel();
34-
});
27+
$this->deferred = new Deferred();
3528
}
3629

3730
/**
@@ -48,18 +41,13 @@ public function run(): PromiseInterface
4841
throw new ShouldNotHappenException('Failed creating temp file for stderr.');
4942
}
5043

51-
$this->process = new Process($this->command, fds: [
44+
$process = new Process($this->command, fds: [
5245
1 => $tmpStdOutResource,
5346
2 => $tmpStdErrResource,
5447
]);
55-
$this->process->start($this->loop);
48+
$process->start($this->loop);
5649

57-
$this->process->on('exit', function ($exitCode) use ($tmpStdOutResource, $tmpStdErrResource): void {
58-
if ($this->canceled) {
59-
fclose($tmpStdOutResource);
60-
fclose($tmpStdErrResource);
61-
return;
62-
}
50+
$process->on('exit', function ($exitCode) use ($tmpStdOutResource, $tmpStdErrResource): void {
6351
rewind($tmpStdOutResource);
6452
$stdOut = stream_get_contents($tmpStdOutResource);
6553
fclose($tmpStdOutResource);
@@ -84,14 +72,4 @@ public function run(): PromiseInterface
8472
return $this->deferred->promise();
8573
}
8674

87-
private function cancel(): void
88-
{
89-
if ($this->process === null) {
90-
throw new ShouldNotHappenException('Cancelling process before running');
91-
}
92-
$this->canceled = true;
93-
$this->process->terminate();
94-
$this->deferred->reject(new ProcessCanceledException());
95-
}
96-
9775
}

0 commit comments

Comments
 (0)