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

declare(strict_types=1);

namespace Rector\Bench\Source;

use Countable;
use DateTime;
use DateTimeImmutable;

/**
* Fixture parsed by short_name_resolver_bench.php. Deliberately name-heavy:
* many fully-qualified references plus docblock types, so resolveFromFile() does real work.
* References only real classes so it stays valid if any whole-repo check scans it.
*/
final class ManyNamesSource implements Countable
{
/**
* @var \PhpParser\Node\Stmt\Class_[]
*/
private array $classes = [];

/**
* @param \PhpParser\Node\Name $name
* @return \PhpParser\Node\Expr\New_
*/
public function first(\PhpParser\Node\Name $name): \PhpParser\Node\Expr\New_
{
$a = new \PhpParser\Node\Scalar\String_('a');
$b = new \PhpParser\Node\Identifier('b');
$c = new DateTimeImmutable();

return new \PhpParser\Node\Expr\New_($name);
}

/**
* @param \PhpParser\Node\Stmt\Property $property
* @return \PhpParser\Comment\Doc
*/
public function second(\PhpParser\Node\Stmt\Property $property): \PhpParser\Comment\Doc
{
$x = new \PhpParser\Node\Stmt\ClassMethod('x');
$y = new \SplStack();
$z = new \ArrayObject();

return new \PhpParser\Comment\Doc('/** */');
}

/**
* @param \PhpParser\Node\Arg $arg
* @return \PhpParser\Node\Expr\MethodCall
*/
public function third(\PhpParser\Node\Arg $arg): \PhpParser\Node\Expr\MethodCall
{
$p = new \RuntimeException();
$q = new \LogicException();
$r = new DateTime();

return new \PhpParser\Node\Expr\MethodCall(new \PhpParser\Node\Expr\Variable('v'), 'm');
}

/**
* @param \PhpParser\Node\Stmt\Use_ $use
* @return \PhpParser\Node\Stmt\Namespace_
*/
public function fourth(\PhpParser\Node\Stmt\Use_ $use): \PhpParser\Node\Stmt\Namespace_
{
$s = new \SplObjectStorage();
$t = new \DateInterval('P1D');
$u = new \PhpParser\Node\Scalar\Int_(1);

return new \PhpParser\Node\Stmt\Namespace_(new \PhpParser\Node\Name('N'));
}

public function count(): int
{
return count($this->classes);
}
}
117 changes: 117 additions & 0 deletions bench/short_name_resolver_bench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

/**
* Benchmark for ShortNameResolver::resolveFromFile() caching.
*
* Simulates the file-processing convergence loop: the same file is re-queried across
* several passes, with its stmts version bumped between passes (as real rules do when
* they change nodes). Two access patterns are measured:
*
* - single-version: one version, many per-node queries (isolates per-node caching)
* - multi-version: version bumped each pass, many per-node queries per pass
* (this is where the filePath-cached vs version-cached behavior differs)
*
* Run on both branches to compare:
* php bench/short_name_resolver_bench.php
*
* 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 ITERATIONS = 8; // convergence passes (stmts version bumps)
const NODES = 400; // resolveFromFile() calls per pass (one per FullyQualified node)
const REPEAT = 25; // 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';

// shared stateless dependencies; a fresh ShortNameResolver is built per repeat so its cache starts empty
$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;
};

/**
* @param callable(): ShortNameResolver $makeResolver
*/
$run = static function (bool $bumpVersionEachPass) use (
$makeResolver,
$testingParser,
$sourceFilePath
): void {
$resolver = $makeResolver();
$file = $testingParser->parseFilePathToFile($sourceFilePath);

for ($pass = 0; $pass < ITERATIONS; ++$pass) {
if ($bumpVersionEachPass) {
// mark the stmts as changed, exactly as a rule that edited the file does
$file->changeNewStmts($file->getNewStmts());
}

for ($node = 0; $node < NODES; ++$node) {
$resolver->resolveFromFile($file);
}
}
};

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

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

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

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

assert(class_exists(File::class));

$singleVersion = $measure(false);
$multiVersion = $measure(true);

printf('ShortNameResolver::resolveFromFile benchmark%s', PHP_EOL);
printf(' iterations=%d nodes=%d repeat=%d%s', ITERATIONS, NODES, REPEAT, PHP_EOL);
printf('%s', PHP_EOL);
printf(' single-version cpu=%.4fs wall=%.4fs%s', $singleVersion['cpu'], $singleVersion['wall'], PHP_EOL);
printf(' multi-version cpu=%.4fs wall=%.4fs%s', $multiVersion['cpu'], $multiVersion['wall'], PHP_EOL);
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ public function testResolveShortClassLikeNamesAgainAfterStmtsChange(): void
$this->assertSame(['RenamedClass'], $this->shortNameResolver->resolveShortClassLikeNames($file));
}

public function testResolveFromFileAgainAfterStmtsChange(): void
{
$file = $this->testingParser->parseFilePathToFile(__DIR__ . '/Fixture/various_imports.php.inc');
$this->assertArrayHasKey('VariousImports', $this->shortNameResolver->resolveFromFile($file));

$class = new NodeFinder()
->findFirstInstanceOf($file->getNewStmts(), Class_::class);
$this->assertInstanceOf(Class_::class, $class);

$class->name = new Identifier('RenamedClass');
$file->changeNewStmts($file->getNewStmts());

$shortNamesAfterChange = $this->shortNameResolver->resolveFromFile($file);
$this->assertArrayNotHasKey('VariousImports', $shortNamesAfterChange);
$this->assertArrayHasKey('RenamedClass', $shortNamesAfterChange);
}

public function testResolveShortClassLikeNamesPerFile(): void
{
$firstFile = $this->testingParser->parseFilePathToFile(__DIR__ . '/Fixture/various_imports.php.inc');
Expand Down
22 changes: 13 additions & 9 deletions rules/CodingStyle/ClassNameImport/ShortNameResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@
*/
final class ShortNameResolver
{
private ?File $shortNamesFile = null;

private int $shortNamesVersion = 0;

/**
* @var array<string, string[]>
* @var array<string, string>
*/
private array $shortNamesByFilePath = [];
private array $shortNamesToFullyQualifiedNames = [];

private ?File $shortClassLikeNamesFile = null;

Expand All @@ -54,16 +58,16 @@ public function __construct(
*/
public function resolveFromFile(File $file): array
{
$filePath = $file->getFilePath();

if (isset($this->shortNamesByFilePath[$filePath])) {
return $this->shortNamesByFilePath[$filePath];
// re-resolve when the file stmts change, so later iterations don't read a stale map
if ($file === $this->shortNamesFile && $file->getNewStmtsVersion() === $this->shortNamesVersion) {
return $this->shortNamesToFullyQualifiedNames;
}

$shortNamesToFullyQualifiedNames = $this->resolveForStmts($file->getNewStmts());
$this->shortNamesByFilePath[$filePath] = $shortNamesToFullyQualifiedNames;
$this->shortNamesFile = $file;
$this->shortNamesVersion = $file->getNewStmtsVersion();
$this->shortNamesToFullyQualifiedNames = $this->resolveForStmts($file->getNewStmts());

return $shortNamesToFullyQualifiedNames;
return $this->shortNamesToFullyQualifiedNames;
}

/**
Expand Down
Loading