diff --git a/bench/unused_import_resolve_bench.php b/bench/unused_import_resolve_bench.php new file mode 100644 index 00000000000..da68e830e5a --- /dev/null +++ b/bench/unused_import_resolve_bench.php @@ -0,0 +1,95 @@ +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); diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index d3ef58d3c3d..e8ae700c416 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -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; } @@ -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; @@ -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; } @@ -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,