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

declare(strict_types=1);

/**
* Benchmark for RenameClassRector::shouldSkipClassConstFetchForMissingConstantName().
*
* For every fully-qualified class-const fetch node the guard used to scan the whole configured
* rename map, running isName() on each entry to find the one matching the node. Upgrade sets
* configure thousands of class renames, so this is a per-node linear scan.
*
* The change looks the node name up in the map directly (the map is keyed by old class, and a
* node has a single name), the same hash lookup the rename itself already uses.
*
* This bench drives refactor() over many real class-const fetches (\DateTime::ATOM) that are NOT
* in the rename map - the worst case, where the old code scans every entry before giving up.
*
* Run on both branches to compare:
* php bench/rename_class_const_fetch_lookup_bench.php
*
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy under
* concurrent load.
*/

use PhpParser\Node\Name\FullyQualified;
use Rector\DependencyInjection\LazyContainerFactory;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Testing\TestingParser\TestingParser;

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

const RENAMES = 500; // configured class renames, none matching the node below
const FETCHES = 150; // class-const fetches per pass
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(RenameClassRector::class);

$configuration = [];
for ($i = 0; $i < RENAMES; ++$i) {
$configuration['Bench\\SomeOldClass' . $i] = 'Bench\\SomeNewClass' . $i;
}

$rector->configure($configuration);

$source = "<?php\n\nnamespace Bench;\n\n";
for ($i = 0; $i < FETCHES; ++$i) {
// real class + real constant, class-const fetch, deliberately absent from the rename map
$source .= "\$x{$i} = \\DateTime::ATOM;\n";
}

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

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

/** @var FullyQualified[] $fullyQualifieds */
$fullyQualifieds = $betterNodeFinder->findInstanceOf($stmts, FullyQualified::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, $fullyQualifieds): void {
foreach ($fullyQualifieds as $fullyQualified) {
$rector->refactor($fullyQualified);
}
};

// 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($fullyQualifieds);

printf('renames: %d, fetches: %d, repeats: %d, refactor() calls: %d%s', RENAMES, count($fullyQualifieds), 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);
31 changes: 14 additions & 17 deletions rules/Renaming/Rector/Name/RenameClassRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ private function shouldSkipClassConstFetchForMissingConstantName(
FullyQualified $fullyQualified,
array $oldToNewClasses
): bool {
if (! $this->reflectionProvider->hasClass($fullyQualified->toString())) {
$oldClass = $fullyQualified->toString();
if (! $this->reflectionProvider->hasClass($oldClass)) {
return false;
}

Expand All @@ -149,24 +150,20 @@ private function shouldSkipClassConstFetchForMissingConstantName(
return false;
}

foreach ($oldToNewClasses as $oldClass => $newClass) {
if (! $this->isName($fullyQualified, $oldClass)) {
continue;
}

if (! $this->reflectionProvider->hasClass($newClass)) {
continue;
}

$classReflection = $this->reflectionProvider->getClass($newClass);
$oldClassReflection = $this->reflectionProvider->getClass($oldClass);
// the node name is looked up directly, same as the rename itself does, instead of scanning the whole map
$newClass = $oldToNewClasses[$oldClass] ?? null;
if ($newClass === null) {
return false;
}

if ($oldClassReflection->hasConstant($constFetchName) && ! $classReflection->hasConstant($constFetchName)) {
// should be skipped as new class does not have access to the constant
return true;
}
if (! $this->reflectionProvider->hasClass($newClass)) {
return false;
}

return false;
$classReflection = $this->reflectionProvider->getClass($newClass);
$oldClassReflection = $this->reflectionProvider->getClass($oldClass);

// should be skipped as new class does not have access to the constant
return $oldClassReflection->hasConstant($constFetchName) && ! $classReflection->hasConstant($constFetchName);
}
}
Loading