diff --git a/bench/Source/ManyNamesSource.php b/bench/Source/ManyNamesSource.php new file mode 100644 index 00000000000..4f08e1c71fc --- /dev/null +++ b/bench/Source/ManyNamesSource.php @@ -0,0 +1,79 @@ +classes); + } +} diff --git a/bench/short_name_resolver_bench.php b/bench/short_name_resolver_bench.php new file mode 100644 index 00000000000..f0e565cb0c5 --- /dev/null +++ b/bench/short_name_resolver_bench.php @@ -0,0 +1,117 @@ +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); diff --git a/rules-tests/CodingStyle/ClassNameImport/ShortNameResolver/ShortNameResolverTest.php b/rules-tests/CodingStyle/ClassNameImport/ShortNameResolver/ShortNameResolverTest.php index 504f5b6679a..1be437a79e2 100644 --- a/rules-tests/CodingStyle/ClassNameImport/ShortNameResolver/ShortNameResolverTest.php +++ b/rules-tests/CodingStyle/ClassNameImport/ShortNameResolver/ShortNameResolverTest.php @@ -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'); diff --git a/rules/CodingStyle/ClassNameImport/ShortNameResolver.php b/rules/CodingStyle/ClassNameImport/ShortNameResolver.php index 871833c6aff..faa62da361c 100644 --- a/rules/CodingStyle/ClassNameImport/ShortNameResolver.php +++ b/rules/CodingStyle/ClassNameImport/ShortNameResolver.php @@ -26,10 +26,14 @@ */ final class ShortNameResolver { + private ?File $shortNamesFile = null; + + private int $shortNamesVersion = 0; + /** - * @var array + * @var array */ - private array $shortNamesByFilePath = []; + private array $shortNamesToFullyQualifiedNames = []; private ?File $shortClassLikeNamesFile = null; @@ -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; } /**