From cd96b35c99c2fba57b93f7bfab9313389d9a2593 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 18 Oct 2026 15:31:00 +0200 Subject: [PATCH] [perf] Collect short names in a single AST walk in ShortNameResolver resolveFromFile() built its short-name map with two separate full-AST traversals of the same stmts - one for class-like and Name nodes, one for docblock short names. The map is version-cached, so during a convergence loop it is rebuilt once per stmts-version bump, paying for two traversals each time. Merge both into a single traversal: the docblock short names are collected in the same callback, then fqnized and merged afterwards. Result is identical, the stmts tree is walked once instead of twice. Adds bench/short_name_resolver_traversal_bench.php, which forces a re-resolve on every call to isolate the rebuild cost. --- bench/short_name_resolver_traversal_bench.php | 98 +++++++++++++++++++ .../ClassNameImport/ShortNameResolver.php | 67 +++++++------ 2 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 bench/short_name_resolver_traversal_bench.php diff --git a/bench/short_name_resolver_traversal_bench.php b/bench/short_name_resolver_traversal_bench.php new file mode 100644 index 00000000000..605f7c485cf --- /dev/null +++ b/bench/short_name_resolver_traversal_bench.php @@ -0,0 +1,98 @@ +create(); +$rectorConfig->boot(); + +/** @var TestingParser $testingParser */ +$testingParser = $rectorConfig->make(TestingParser::class); + +$sourceFilePath = __DIR__ . '/Source/ManyNamesSource.php'; + +$simpleCallableNodeTraverser = $rectorConfig->make(SimpleCallableNodeTraverser::class); +$nodeNameResolver = $rectorConfig->make(NodeNameResolver::class); +$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class); +$useImportNameMatcher = $rectorConfig->make(UseImportNameMatcher::class); +$phpDocInfoFactory = $rectorConfig->make(PhpDocInfoFactory::class); + +$makeResolver = static fn (): ShortNameResolver => new ShortNameResolver( + $simpleCallableNodeTraverser, + $nodeNameResolver, + $betterNodeFinder, + $useImportNameMatcher, + $phpDocInfoFactory +); + +$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; +}; + +$run = static function () use ($makeResolver, $testingParser, $sourceFilePath): void { + $resolver = $makeResolver(); + $file = $testingParser->parseFilePathToFile($sourceFilePath); + + for ($rebuild = 0; $rebuild < REBUILDS; ++$rebuild) { + // bump version so the next call misses the cache and does the full walk + $file->changeNewStmts($file->getNewStmts()); + $resolver->resolveFromFile($file); + } +}; + +$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('ShortNameResolver::resolveFromFile traversal benchmark%s', PHP_EOL); +printf(' rebuilds=%d repeat=%d%s', REBUILDS, REPEAT, PHP_EOL); +printf('%s', PHP_EOL); +printf(' forced re-resolve cpu=%.4fs wall=%.4fs%s', $result['cpu'], $result['wall'], PHP_EOL); diff --git a/rules/CodingStyle/ClassNameImport/ShortNameResolver.php b/rules/CodingStyle/ClassNameImport/ShortNameResolver.php index faa62da361c..e4cc03494e9 100644 --- a/rules/CodingStyle/ClassNameImport/ShortNameResolver.php +++ b/rules/CodingStyle/ClassNameImport/ShortNameResolver.php @@ -118,10 +118,17 @@ private function findShortClassLikeNames(File $file): array private function resolveForStmts(array $stmts): array { $shortNamesToFullyQualifiedNames = []; + $docBlockShortNames = []; + // single traversal collects both PHP names and docblock short names, so the file stmts are walked once $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $node) use ( - &$shortNamesToFullyQualifiedNames + &$shortNamesToFullyQualifiedNames, + &$docBlockShortNames ): null { + foreach ($this->resolveDocBlockShortNames($node) as $docBlockShortName) { + $docBlockShortNames[] = $docBlockShortName; + } + // class name is used! if ($node instanceof ClassLike && $node->name instanceof Identifier) { $fullyQualifiedName = $this->nodeNameResolver->getName($node); @@ -152,54 +159,46 @@ private function resolveForStmts(array $stmts): array return null; }); - $docBlockShortNamesToFullyQualifiedNames = $this->resolveFromStmtsDocBlocks($stmts); + $docBlockShortNamesToFullyQualifiedNames = $this->fqnizeShortNames($docBlockShortNames, $stmts); /** @var array $result */ $result = [...$shortNamesToFullyQualifiedNames, ...$docBlockShortNamesToFullyQualifiedNames]; return $result; } /** - * @param Stmt[] $stmts - * @return array + * @return string[] */ - private function resolveFromStmtsDocBlocks(array $stmts): array + private function resolveDocBlockShortNames(Node $node): array { - $shortNames = []; - $this->simpleCallableNodeTraverser->traverseNodesWithCallable($stmts, function (Node $node) use ( - &$shortNames - ): null { - // speed up for nodes that are - $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); - if (! $phpDocInfo instanceof PhpDocInfo) { - return null; - } - - $phpDocNodeTraverser = new PhpDocNodeTraverser(); - $phpDocNodeTraverser->traverseWithCallable( - $phpDocInfo->getPhpDocNode(), - '', - static function ($node) use (&$shortNames): null { - if ($node instanceof PhpDocTagNode) { - $shortName = trim($node->name, '@'); - if (ucfirst($shortName) === $shortName) { - $shortNames[] = $shortName; - } - - return null; - } + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return []; + } - if ($node instanceof IdentifierTypeNode) { - $shortNames[] = $node->name; + $shortNames = []; + $phpDocNodeTraverser = new PhpDocNodeTraverser(); + $phpDocNodeTraverser->traverseWithCallable( + $phpDocInfo->getPhpDocNode(), + '', + static function ($node) use (&$shortNames): null { + if ($node instanceof PhpDocTagNode) { + $shortName = trim($node->name, '@'); + if (ucfirst($shortName) === $shortName) { + $shortNames[] = $shortName; } return null; } - ); - return null; - }); + if ($node instanceof IdentifierTypeNode) { + $shortNames[] = $node->name; + } + + return null; + } + ); - return $this->fqnizeShortNames($shortNames, $stmts); + return $shortNames; } /**