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

declare(strict_types=1);

/**
* Benchmark for RenameMethodRector.
*
* The rule is configured with a list of method renames. For every method/static call node it used
* to scan the whole list, running isStringName() on each entry before the expensive type/reflection
* checks. With a large configured list (symfony/doctrine sets ship hundreds) this is a per-node
* linear scan whose cost is pure string comparison self time.
*
* The change buckets the renames by lowercased old-method name at configure() time, so a node only
* looks at the few renames sharing its name - one hash lookup instead of N isStringName() calls.
*
* This bench configures the rule with many renames, then drives refactor() over many calls whose
* names are NOT in the map (the common case), isolating the scan cost from type resolution.
*
* Run on both branches to compare:
* php bench/rename_method_bucket_bench.php
*
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy under
* concurrent load.
*/

use PhpParser\Node\Expr\MethodCall;
use Rector\DependencyInjection\LazyContainerFactory;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\ValueObject\MethodCallRename;
use Rector\Testing\TestingParser\TestingParser;

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

const RENAMES = 400; // configured method renames
const CALLS = 200; // method calls per pass, names not in the map
const REPEAT = 200; // outer repeats over the whole set

$rectorConfig = (new LazyContainerFactory())->create();
$rectorConfig->boot();

/** @var TestingParser $testingParser */
$testingParser = $rectorConfig->make(TestingParser::class);

/** @var BetterNodeFinder $betterNodeFinder */
$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class);

$rector = $rectorConfig->make(RenameMethodRector::class);

$methodCallRenames = [];
for ($i = 0; $i < RENAMES; ++$i) {
$methodCallRenames[] = new MethodCallRename('Bench\\SomeClass' . $i, 'oldMethod' . $i, 'newMethod' . $i);
}

$rector->configure($methodCallRenames);

$source = "<?php\n\nnamespace Bench;\n\n\$value = new Other();\n";
for ($i = 0; $i < CALLS; ++$i) {
// names deliberately absent from the configured renames
$source .= "\$value->freshCall{$i}();\n";
}

$sourceFilePath = tempnam(sys_get_temp_dir(), 'bench_') . '.php';
file_put_contents($sourceFilePath, $source);

$stmts = $testingParser->parseFileToDecoratedNodes($sourceFilePath);

/** @var MethodCall[] $methodCalls */
$methodCalls = $betterNodeFinder->findInstanceOf($stmts, MethodCall::class);

$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 ($rector, $methodCalls): void {
foreach ($methodCalls as $methodCall) {
$rector->refactor($methodCall);
}
};

// warm up autoloading/reflection caches once, excluded from timing
$run();

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

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

$cpu = $cpuSeconds() - $cpuStart;
$wall = microtime(true) - $wallStart;

@unlink($sourceFilePath);

$totalCalls = REPEAT * count($methodCalls);

printf('renames: %d, calls: %d, repeats: %d, refactor() calls: %d%s', RENAMES, count($methodCalls), REPEAT, $totalCalls, PHP_EOL);
printf('CPU: %.4f s (%.3f us/call)%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL);
printf('wall: %.4f s%s', $wall, PHP_EOL);
15 changes: 10 additions & 5 deletions rules/Renaming/Rector/MethodCall/RenameMethodRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
final class RenameMethodRector extends AbstractRector implements ConfigurableRectorInterface
{
/**
* @var MethodCallRenameInterface[]
* @var array<string, MethodCallRenameInterface[]>
*/
private array $methodCallRenames = [];
private array $methodCallRenamesByOldMethod = [];

public function __construct(
private readonly ClassManipulator $classManipulator,
Expand Down Expand Up @@ -101,7 +101,12 @@ public function configure(array $configuration): void
{
Assert::allIsAOf($configuration, MethodCallRenameInterface::class);

$this->methodCallRenames = $configuration;
// bucket by old method name, so a node matches only the few renames sharing its name,
// instead of scanning the whole configured list per node
$this->methodCallRenamesByOldMethod = [];
foreach ($configuration as $methodCallRename) {
$this->methodCallRenamesByOldMethod[strtolower($methodCallRename->getOldMethod())][] = $methodCallRename;
}
}

private function shouldSkipClassMethod(
Expand Down Expand Up @@ -176,7 +181,7 @@ private function refactorClass(
continue;
}

foreach ($this->methodCallRenames as $methodCallRename) {
foreach ($this->methodCallRenamesByOldMethod[strtolower($methodName)] ?? [] as $methodCallRename) {
if ($this->shouldSkipRename(
$methodName,
$classMethod,
Expand Down Expand Up @@ -238,7 +243,7 @@ private function refactorMethodCallAndStaticCall(
return null;
}

foreach ($this->methodCallRenames as $methodCallRename) {
foreach ($this->methodCallRenamesByOldMethod[strtolower($callName)] ?? [] as $methodCallRename) {
if (! $this->nodeNameResolver->isStringName($callName, $methodCallRename->getOldMethod())) {
continue;
}
Expand Down
31 changes: 16 additions & 15 deletions src/BetterPhpDocParser/PhpDocManipulator/PhpDocClassRenamer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
use Rector\Enum\ClassName;
use Rector\Renaming\Collector\RenamedNameCollector;

final readonly class PhpDocClassRenamer
final class PhpDocClassRenamer
{
/**
* @var array<string, string>
*/
private array $wordBoundaryPatternByOldClass = [];

public function __construct(
private ClassAnnotationMatcher $classAnnotationMatcher,
private RenamedNameCollector $renamedNameCollector
private readonly ClassAnnotationMatcher $classAnnotationMatcher,
private readonly RenamedNameCollector $renamedNameCollector
) {
}

Expand Down Expand Up @@ -130,6 +135,12 @@ private function processSerializerTypeTagValueNode(

$classNameArrayItemNode = $doctrineAnnotationTagValueNode->getSilentValue();

// loop-invariant, resolve once instead of per configured class
$currentTypeArrayItemNode = $doctrineAnnotationTagValueNode->getValue('type');
$currentTypeStringNode = $currentTypeArrayItemNode instanceof ArrayItemNode
? $currentTypeArrayItemNode->value
: null;

foreach ($oldToNewClasses as $oldClass => $newClass) {
if ($classNameArrayItemNode instanceof ArrayItemNode && $classNameArrayItemNode->value instanceof StringNode) {
$classNameStringNode = $classNameArrayItemNode->value;
Expand All @@ -142,7 +153,7 @@ private function processSerializerTypeTagValueNode(
$this->renamedNameCollector->add($oldClass);
$classNameStringNode->value = Strings::replace(
$classNameStringNode->value,
'#\b' . preg_quote($oldClass, '#') . '\b#',
$this->wordBoundaryPatternByOldClass[$oldClass] ??= '#\b' . preg_quote($oldClass, '#') . '\b#',
$newClass
);

Expand All @@ -151,17 +162,7 @@ private function processSerializerTypeTagValueNode(
$hasChanged = true;
}

$currentTypeArrayItemNode = $doctrineAnnotationTagValueNode->getValue('type');
if (! $currentTypeArrayItemNode instanceof ArrayItemNode) {
continue;
}

$currentTypeStringNode = $currentTypeArrayItemNode->value;
if (! $currentTypeStringNode instanceof StringNode) {
continue;
}

if ($currentTypeStringNode->value === $oldClass) {
if ($currentTypeStringNode instanceof StringNode && $currentTypeStringNode->value === $oldClass) {
$currentTypeStringNode->value = $newClass;

$hasChanged = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@

final readonly class DocBlockClassRenamer
{
private PhpDocNodeTraverser $phpDocNodeTraverser;

public function __construct(
private ClassRenamePhpDocNodeVisitor $classRenamePhpDocNodeVisitor,
) {
// the traverser + visitor are reusable across calls, so build them once
$this->phpDocNodeTraverser = new PhpDocNodeTraverser();
$this->phpDocNodeTraverser->addPhpDocNodeVisitor($this->classRenamePhpDocNodeVisitor);
}

/**
Expand All @@ -26,14 +31,11 @@ public function renamePhpDocType(PhpDocInfo $phpDocInfo, array $oldToNewTypes, N
return false;
}

$phpDocNodeTraverser = new PhpDocNodeTraverser();
$phpDocNodeTraverser->addPhpDocNodeVisitor($this->classRenamePhpDocNodeVisitor);

$this->classRenamePhpDocNodeVisitor->setCurrentPhpNode($currentPhpNode);

$this->classRenamePhpDocNodeVisitor->setOldToNewTypes($oldToNewTypes);

$phpDocNodeTraverser->traverse($phpDocInfo->getPhpDocNode());
$this->phpDocNodeTraverser->traverse($phpDocInfo->getPhpDocNode());

return $this->classRenamePhpDocNodeVisitor->hasChanged();
}
Expand Down
Loading