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
95 changes: 95 additions & 0 deletions bench/unused_import_resolve_bench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

/**
* Benchmark for the UnusedImportRemovingPostRector name-resolution walk (first-pass cost).
*
* To find unused `use` imports, enterNode() resolves every referenced name by walking the file
* stmts. The result is version-cached, so this bench bumps the stmts version before every call,
* forcing the walk to run each time (no cache hits) - it isolates the raw first-pass cost, which
* is where merging the former two walks (PHP names + docblock names) into one shows up.
*
* Run on both branches to compare:
* php bench/unused_import_resolve_bench.php
*
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy
* under concurrent load.
*/

use PhpParser\Node\Stmt\Namespace_;
use Rector\DependencyInjection\LazyContainerFactory;
use Rector\PhpParser\Node\FileNode;
use Rector\PostRector\Rector\UnusedImportRemovingPostRector;
use Rector\Testing\TestingParser\TestingParser;
use Rector\ValueObject\Application\File;

require __DIR__ . '/../vendor/autoload.php';

const RESOLVES = 400; // forced walks per run (version bumped each time)
const REPEAT = 40; // outer repeats

$rectorConfig = (new LazyContainerFactory())->create();
$rectorConfig->boot();

/** @var TestingParser $testingParser */
$testingParser = $rectorConfig->make(TestingParser::class);

/** @var UnusedImportRemovingPostRector $postRector */
$postRector = $rectorConfig->make(UnusedImportRemovingPostRector::class);

$sourceFilePath = __DIR__ . '/Source/ManyNamesSource.php';

$cpuSeconds = static function (): float {
$usage = getrusage();
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
};

$file = $testingParser->parseFilePathToFile($sourceFilePath);
$postRector->setFile($file);

$fileNode = $file->getFileNode();
assert($fileNode instanceof FileNode);

// the namespaced root that actually carries the use statements
$rootNode = $fileNode;
foreach ($fileNode->stmts as $stmt) {
if ($stmt instanceof Namespace_) {
$rootNode = $stmt;
}
}

$run = static function () use ($postRector, $rootNode, $file): void {
for ($i = 0; $i < RESOLVES; ++$i) {
// bump version so the next enterNode misses the cache and does the full walk
$file->changeNewStmts($file->getNewStmts());
$postRector->enterNode($rootNode);
}
};

$measure = static function () use ($run, $cpuSeconds): array {
// warm up autoloading / parser once, excluded from timing
$run();

$cpuStart = $cpuSeconds();
$wallStart = hrtime(true);

for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
$run();
}

return [
'cpu' => $cpuSeconds() - $cpuStart,
'wall' => (hrtime(true) - $wallStart) / 1_000_000_000,
];
};

assert(class_exists(File::class));

$result = $measure();

printf('UnusedImportRemovingPostRector resolve-walk benchmark%s', PHP_EOL);
printf(' resolves=%d repeat=%d%s', RESOLVES, REPEAT, PHP_EOL);
printf('%s', PHP_EOL);
printf(' forced walk cpu=%.4fs wall=%.4fs%s', $result['cpu'], $result['wall'], PHP_EOL);
81 changes: 40 additions & 41 deletions src/PostRector/Rector/UnusedImportRemovingPostRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,22 @@ private function hasUseStmt(array $stmts): bool
}

/**
* Single walk that collects both the referenced PHP names and the docblock class names,
* so the file stmts are traversed once instead of twice.
*
* @return string[]
*/
private function findNonUseImportNames(Namespace_|FileNode $fileNode): array
private function findUsedNames(Namespace_|FileNode $rootNode): array
{
$names = [];
// the root node's own docblock (the former doc walk traversed the root node, not just its stmts)
$names = $this->resolveDocBlockNames($rootNode);

$this->simpleCallableNodeTraverser->traverseNodesWithCallable($fileNode->stmts, static function (Node $node) use (
$this->simpleCallableNodeTraverser->traverseNodesWithCallable($rootNode->stmts, function (Node $node) use (
&$names
): int|null|Name {
): int|null {
// collect the node's docblock first, so a Use_ node's own doc is still read before we stop descending
$names = [...$names, ...$this->resolveDocBlockNames($node)];

if ($node instanceof Use_) {
return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
Expand All @@ -143,12 +150,12 @@ private function findNonUseImportNames(Namespace_|FileNode $fileNode): array
if ($originalName instanceof Name) {
// collect original Name as cover namespaced used
$names[] = $originalName->toString();
return $node;
return null;
}
}

$names[] = $node->toString();
return $node;
return null;
});

return $names;
Expand All @@ -157,46 +164,41 @@ private function findNonUseImportNames(Namespace_|FileNode $fileNode): array
/**
* @return string[]
*/
private function findNamesInDocBlocks(Namespace_|FileNode $rootNode): array
private function resolveDocBlockNames(Node $node): array
{
$names = [];
$comments = $node->getComments();
if ($comments === []) {
return [];
}

$this->simpleCallableNodeTraverser->traverseNodesWithCallable($rootNode, function (Node $node) use (
&$names
) {
$comments = $node->getComments();
if ($comments === []) {
return null;
}
$docs = array_filter($comments, static fn (Comment $comment): bool => $comment instanceof Doc);
if ($docs === []) {
return [];
}

$docs = array_filter($comments, static fn (Comment $comment): bool => $comment instanceof Doc);
if ($docs === []) {
return null;
$names = [];
$totalDocs = count($docs);
foreach ($docs as $doc) {
$nodeToCheck = $totalDocs === 1 ? $node : clone $node;
if ($totalDocs > 1) {
$nodeToCheck->setDocComment($doc);
}

$totalDocs = count($docs);
foreach ($docs as $doc) {
$nodeToCheck = $totalDocs === 1 ? $node : clone $node;
if ($totalDocs > 1) {
$nodeToCheck->setDocComment($doc);
}

$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($nodeToCheck);
$names = [...$names, ...$phpDocInfo->getAnnotationClassNames()];
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($nodeToCheck);
$names = [...$names, ...$phpDocInfo->getAnnotationClassNames()];

$constFetchNodeNames = $phpDocInfo->getConstFetchNodeClassNames();
$names = [...$names, ...$constFetchNodeNames];
$constFetchNodeNames = $phpDocInfo->getConstFetchNodeClassNames();
$names = [...$names, ...$constFetchNodeNames];

$genericTagClassNames = $phpDocInfo->getGenericTagClassNames();
$names = [...$names, ...$genericTagClassNames];
$genericTagClassNames = $phpDocInfo->getGenericTagClassNames();
$names = [...$names, ...$genericTagClassNames];

$inlineGenericUsesTagClassNames = $phpDocInfo->getInlineGenericUsesTagClassNames();
$names = [...$names, ...$inlineGenericUsesTagClassNames];
$inlineGenericUsesTagClassNames = $phpDocInfo->getInlineGenericUsesTagClassNames();
$names = [...$names, ...$inlineGenericUsesTagClassNames];

$arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames();
$names = [...$names, ...$arrayItemTagClassNames];
}
});
$arrayItemTagClassNames = $phpDocInfo->getArrayItemNodeClassNames();
$names = [...$names, ...$arrayItemTagClassNames];
}

return $names;
}
Expand All @@ -222,10 +224,7 @@ private function resolveUsedPhpAndDocNames(Namespace_|FileNode $rootNode): array
return $cached['names'];
}

$phpNames = $this->findNonUseImportNames($rootNode);
$docBlockNames = $this->findNamesInDocBlocks($rootNode);

$names = array_unique([...$phpNames, ...$docBlockNames]);
$names = array_unique($this->findUsedNames($rootNode));
$this->usedNamesCache[$objectId] = [
'version' => $version,
'names' => $names,
Expand Down
Loading