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

declare(strict_types=1);

/**
* Benchmark for ShortNameResolver::resolveFromFile() traversal cost.
*
* resolveFromFile() builds the short-name map by walking the file stmts. The map is
* version-cached, so during a convergence loop it is rebuilt once per stmts version bump.
* This bench isolates that rebuild cost: the stmts version is bumped before every call,
* so every call does the full walk (no cache hits).
*
* Use it to compare the single-walk vs double-walk implementations of resolveForStmts():
* php bench/short_name_resolver_traversal_bench.php # on each branch
*
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy
* under concurrent load.
*/

use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\CodingStyle\ClassNameImport\ShortNameResolver;
use Rector\CodingStyle\NodeAnalyzer\UseImportNameMatcher;
use Rector\DependencyInjection\LazyContainerFactory;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\Testing\TestingParser\TestingParser;
use Rector\ValueObject\Application\File;

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

const REBUILDS = 200; // forced re-resolves (each one does a full walk)
const REPEAT = 40; // outer repeats, each with a fresh-cache resolver

$rectorConfig = (new LazyContainerFactory())->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);
67 changes: 33 additions & 34 deletions rules/CodingStyle/ClassNameImport/ShortNameResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -152,54 +159,46 @@ private function resolveForStmts(array $stmts): array
return null;
});

$docBlockShortNamesToFullyQualifiedNames = $this->resolveFromStmtsDocBlocks($stmts);
$docBlockShortNamesToFullyQualifiedNames = $this->fqnizeShortNames($docBlockShortNames, $stmts);
/** @var array<string, string> $result */
$result = [...$shortNamesToFullyQualifiedNames, ...$docBlockShortNamesToFullyQualifiedNames];
return $result;
}

/**
* @param Stmt[] $stmts
* @return array<string, string>
* @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;
}

/**
Expand Down
Loading