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
77 changes: 8 additions & 69 deletions src/Analyser/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_push;
use function array_unique;
use function array_values;
use function count;
use function getcwd;
use function in_array;
use function is_dir;
Expand Down Expand Up @@ -1290,91 +1288,32 @@ private function collectAnalysisNodes(
?AnalyserOptions $analyserOptions = null,
bool $withFileAnalysis = true,
): ExtractionResult {
$classNodes = [];
$fileAnalyses = [];
$anonymousClassNodes = [];
$fileReferences = [];
$fileInstantiations = [];
$functionNodes = [];
$anonymousFunctionNodes = [];
$filesToParse = [];

foreach ($files as $file) {
$cachedResult = $withFileAnalysis
? $this->analysisResultCache?->loadAnalysisNodesWithFileAnalysis(
$file,
$this->analysisNodeCacheNamespace
)
: $this->analysisResultCache?->loadAnalysisNodes($file, $this->analysisNodeCacheNamespace);

if ($cachedResult === null) {
$filesToParse[] = $file;
continue;
}

array_push($classNodes, ...$cachedResult['classNodes']);
array_push($anonymousClassNodes, ...$cachedResult['anonymousClassNodes']);
array_push($functionNodes, ...$cachedResult['functionNodes']);
array_push($anonymousFunctionNodes, ...$cachedResult['anonymousFunctionNodes']);

$fileReferences[$file] = $cachedResult['fileReferences'];
$fileInstantiations[$file] = $cachedResult['fileInstantiations'];

if (isset($cachedResult['fileAnalysis'])) {
$fileAnalyses[$file] = $cachedResult['fileAnalysis'];
}
}

$progressHandler?->start(count($filesToParse));

if ($filesToParse === []) {
$progressHandler?->finish();

return new ExtractionResult(
$classNodes,
$fileAnalyses,
$anonymousClassNodes,
$fileReferences,
$fileInstantiations,
$functionNodes,
$anonymousFunctionNodes,
);
}

$options = $analyserOptions ?? AnalyserOptions::parallel();

// Each extractor hydrates its files' node-cache payloads itself, so in
// parallel mode that work is split across the workers instead of being
// done serially here before any worker starts.
if ($options->isParallel()) {
// Workers write their own files' cache payloads while other workers are
// still parsing, instead of the coordinator doing it serially afterwards.
$parsedResult = (new ParallelAnalysisNodeExtractor(
$extractionResult = (new ParallelAnalysisNodeExtractor(
$this->basePath,
$layers,
$layerPatterns,
$options->workerCount,
$this->analysisResultCache?->getCacheDirectory(),
$this->analysisResultCache,
$this->analysisNodeCacheNamespace,
))->extract($filesToParse, $progressHandler, $withFileAnalysis);
))->extract($files, $progressHandler, $withFileAnalysis);
} else {
$parsedResult = (new AnalysisNodeExtractor(
$extractionResult = (new AnalysisNodeExtractor(
$chainLayerResolver,
analysisResultCache: $this->analysisResultCache,
analysisNodeCacheNamespace: $this->analysisNodeCacheNamespace,
))->extract($filesToParse, $progressHandler, $withFileAnalysis);
))->extract($files, $progressHandler, $withFileAnalysis);
}

$progressHandler?->finish();

// Cached nodes first, then the freshly parsed ones.
return new ExtractionResult(
classNodes: [...$classNodes, ...$parsedResult->classNodes],
fileAnalyses: $fileAnalyses + $parsedResult->fileAnalyses,
anonymousClassNodes: [...$anonymousClassNodes, ...$parsedResult->anonymousClassNodes],
fileReferences: $fileReferences + $parsedResult->fileReferences,
fileInstantiations: $fileInstantiations + $parsedResult->fileInstantiations,
functionNodes: [...$functionNodes, ...$parsedResult->functionNodes],
anonymousFunctionNodes: [...$anonymousFunctionNodes, ...$parsedResult->anonymousFunctionNodes],
);
return $extractionResult;
}

/**
Expand Down
88 changes: 78 additions & 10 deletions src/Analyser/AnalysisNodeExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;

use function array_push;
use function count;

/**
* @internal
*/
Expand All @@ -34,24 +37,33 @@ public function __construct(
$this->fileAnalysisProvider = $fileAnalysisProvider ?? new FileAnalysisProvider();
}

/** @param list<string> $files */
/**
* Only files without a valid node-cache payload are parsed, and only those
* count towards the progress total.
*
* @param list<string> $files
*/
public function extract(
array $files,
?ProgressHandlerInterface $progressHandler = null,
bool $withFileAnalysis = true,
): ExtractionResult {
[$cachedResult, $filesToParse] = $this->loadFromCache($files, $withFileAnalysis);

$progressHandler?->start(count($filesToParse));

$analysisNodeCollector = new AnalysisNodeCollector($this->layerResolver);
$nodeTraverser = new NodeTraverser(new NameResolver(), $analysisNodeCollector);
$fileAnalyses = [];

foreach ($files as $file) {
foreach ($filesToParse as $fileToParse) {
try {
$ast = $this->fileAnalysisProvider->ast($file, $withFileAnalysis);
$ast = $this->fileAnalysisProvider->ast($fileToParse, $withFileAnalysis);
$nonCanonicalKeywordConstants = [];
$numericLiterals = [];

if ($ast !== null && $ast !== []) {
$analysisNodeCollector->setCurrentFile($file, $this->fileAnalysisProvider->tokens());
$analysisNodeCollector->setCurrentFile($fileToParse, $this->fileAnalysisProvider->tokens());
$nodeTraverser->traverse($ast);

$nonCanonicalKeywordConstants = $analysisNodeCollector->getNonCanonicalKeywordConstants();
Expand All @@ -61,18 +73,18 @@ public function extract(
// Analysed after the traversal so the facts only the collector
// records reach the file analysis without a second AST walk.
if ($withFileAnalysis) {
$fileAnalyses[$file] = $this->fileAnalysisProvider->analyse(
$file,
$fileAnalyses[$fileToParse] = $this->fileAnalysisProvider->analyse(
$fileToParse,
$nonCanonicalKeywordConstants,
$numericLiterals,
);
}
} finally {
if ($withFileAnalysis) {
$this->fileAnalysisProvider->releaseAst($file);
$this->fileAnalysisProvider->releaseAst($fileToParse);
}

$progressHandler?->advance($file);
$progressHandler?->advance($fileToParse);
}
}

Expand All @@ -87,11 +99,67 @@ public function extract(
);

$this->analysisResultCache?->storeExtractionResult(
$files,
$filesToParse,
$this->analysisNodeCacheNamespace,
$extractionResult
);

return $extractionResult;
return $cachedResult->merge($extractionResult);
}

/**
* Hydrates every file with a valid node-cache payload; the rest still need parsing.
*
* @param list<string> $files
* @return array{ExtractionResult, list<string>}
*/
private function loadFromCache(array $files, bool $withFileAnalysis): array
{
$classNodes = [];
$fileAnalyses = [];
$anonymousClassNodes = [];
$fileReferences = [];
$fileInstantiations = [];
$functionNodes = [];
$anonymousFunctionNodes = [];
$filesToParse = [];

foreach ($files as $file) {
$cachedResult = $withFileAnalysis
? $this->analysisResultCache?->loadAnalysisNodesWithFileAnalysis(
$file,
$this->analysisNodeCacheNamespace
)
: $this->analysisResultCache?->loadAnalysisNodes($file, $this->analysisNodeCacheNamespace);

if ($cachedResult === null) {
$filesToParse[] = $file;
continue;
}

array_push($classNodes, ...$cachedResult['classNodes']);
array_push($anonymousClassNodes, ...$cachedResult['anonymousClassNodes']);
array_push($functionNodes, ...$cachedResult['functionNodes']);
array_push($anonymousFunctionNodes, ...$cachedResult['anonymousFunctionNodes']);

$fileReferences[$file] = $cachedResult['fileReferences'];
$fileInstantiations[$file] = $cachedResult['fileInstantiations'];

if (isset($cachedResult['fileAnalysis'])) {
$fileAnalyses[$file] = $cachedResult['fileAnalysis'];
}
}

$cachedResult = new ExtractionResult(
$classNodes,
$fileAnalyses,
$anonymousClassNodes,
$fileReferences,
$fileInstantiations,
$functionNodes,
$anonymousFunctionNodes,
);

return [$cachedResult, $filesToParse];
}
}
14 changes: 14 additions & 0 deletions src/Analyser/ExtractionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,18 @@ public function __construct(
public array $anonymousFunctionNodes = [],
) {
}

/** Nodes of this result first, then those of $other. */
public function merge(self $other): self
{
return new self(
[...$this->classNodes, ...$other->classNodes],
$this->fileAnalyses + $other->fileAnalyses,
[...$this->anonymousClassNodes, ...$other->anonymousClassNodes],
$this->fileReferences + $other->fileReferences,
$this->fileInstantiations + $other->fileInstantiations,
[...$this->functionNodes, ...$other->functionNodes],
[...$this->anonymousFunctionNodes, ...$other->anonymousFunctionNodes],
);
}
}
2 changes: 1 addition & 1 deletion src/Analyser/Parallel/AnalysisNodeWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static function run(string $inputFile, string $outputFile, mixed $outputS

$stream = $outputStream ?? STDOUT;

$progressHandler = $emitProgress ? new WorkerProgressHandler($stream) : null;
$progressHandler = $emitProgress ? new WorkerProgressHandler($stream, $files) : null;

$cache = $payload['cache'] ?? null;
/** @var string $cacheNamespace */
Expand Down
55 changes: 37 additions & 18 deletions src/Analyser/Parallel/ParallelAnalysisNodeExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
use function array_fill;
use function array_key_exists;
use function array_keys;
use function array_pop;
use function array_push;
use function array_search;
use function arsort;
use function assert;
use function count;
use function dirname;
use function explode;
use function fclose;
use function feof;
use function fgets;
use function file_put_contents;
use function filesize;
use function fread;
Expand All @@ -39,7 +42,6 @@
use function serialize;
use function sprintf;
use function stream_set_blocking;
use function substr_count;
use function unlink;
use function unserialize;
use function usleep;
Expand Down Expand Up @@ -77,6 +79,8 @@ public function extract(
bool $withFileAnalysis = true,
): ExtractionResult {
if ($files === []) {
$progressHandler?->start(0);

return new ExtractionResult([], []);
}

Expand Down Expand Up @@ -131,20 +135,33 @@ public function extract(
assert(isset($pipes[0]) && isset($pipes[1]));
fclose($pipes[0]);

$stdoutPipe = $pipes[1];
stream_set_blocking($stdoutPipe, false);

$pending[] = [
'process' => $process,
'files' => $chunk,
'filesAdvanced' => 0,
'inputFile' => $inputFile,
'outputFile' => $outputFile,
'stderrFile' => $stderrFile,
'stdoutPipe' => $stdoutPipe,
'process' => $process,
'files' => $chunk,
'buffer' => '',
'inputFile' => $inputFile,
'outputFile' => $outputFile,
'stderrFile' => $stderrFile,
'stdoutPipe' => $pipes[1],
];
}

// A worker's first line is how many of its files it still has to parse
// after hydrating the cached ones; summed, that is the progress total.
if ($emitProgress) {
$totalToParse = 0;

foreach ($pending as $worker) {
$totalToParse += (int) fgets($worker['stdoutPipe']);
}

$progressHandler->start($totalToParse);
}

foreach ($pending as $worker) {
stream_set_blocking($worker['stdoutPipe'], false);
}

$nodes = [];
$fileAnalyses = [];
$anonymousClassNodes = [];
Expand All @@ -162,15 +179,17 @@ public function extract(

$data = fread($stdoutPipe, 8192);
if ($data !== false && $data !== '') {
$workerFiles = $pending[$key]['files'];
$nextFileIdx = $pending[$key]['filesAdvanced'];
$lastFileIdx = min($nextFileIdx + substr_count($data, "\n"), count($workerFiles));
// One chunk index per parsed file; a read may end mid-line.
$lines = explode("\n", $pending[$key]['buffer'] . $data);
$pending[$key]['buffer'] = array_pop($lines);

for (; $nextFileIdx < $lastFileIdx; $nextFileIdx++) {
$progressHandler?->advance($workerFiles[$nextFileIdx]);
}
foreach ($lines as $line) {
$file = $pending[$key]['files'][(int) $line] ?? null;

$pending[$key]['filesAdvanced'] = $nextFileIdx;
if ($file !== null) {
$progressHandler?->advance($file);
}
}

$anyActivity = true;
}
Expand Down
Loading