From c46d31b0eaf14d43a31e3e71030e37b4f2269c83 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 20 Oct 2026 12:08:22 +0200 Subject: [PATCH] [perf] Read caller class name off node in ExportToReflectionFunctionRector --- ...to_reflection_function_name_type_bench.php | 93 +++++++++++++++++++ .../Fixture/skip_other_class.php.inc | 18 ++++ .../ExportToReflectionFunctionRector.php | 21 ++++- 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 bench/export_to_reflection_function_name_type_bench.php create mode 100644 rules-tests/Php74/Rector/StaticCall/ExportToReflectionFunctionRector/Fixture/skip_other_class.php.inc diff --git a/bench/export_to_reflection_function_name_type_bench.php b/bench/export_to_reflection_function_name_type_bench.php new file mode 100644 index 00000000000..30b478b494f --- /dev/null +++ b/bench/export_to_reflection_function_name_type_bench.php @@ -0,0 +1,93 @@ +class), which runs the type resolver over the class name node even + * though the name is already on the node. + * + * The change reads a concrete class name straight off the node (new ObjectType($name)) and only falls + * back to getType() for self/static/parent, which depend on the enclosing class. The isSuperTypeOf() + * check that follows is unchanged, so the decision is identical. + * + * This bench parses a file of many \ReflectionFunction::export('foo') calls, attaches scope (as the + * real run does), then drives refactor() over every StaticCall node. + * + * Run on both branches to compare: + * php bench/export_to_reflection_function_name_type_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\StaticCall; +use Rector\DependencyInjection\LazyContainerFactory; +use Rector\Php74\Rector\StaticCall\ExportToReflectionFunctionRector; +use Rector\PhpParser\Node\BetterNodeFinder; +use Rector\Testing\TestingParser\TestingParser; + +require __DIR__ . '/../vendor/autoload.php'; + +const CALLS = 150; // ReflectionFunction::export() static calls +const REPEAT = 60; // outer repeats over the whole call 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(ExportToReflectionFunctionRector::class); + +$source = "parseFileToDecoratedNodes($sourceFilePath); + +/** @var StaticCall[] $staticCalls */ +$staticCalls = $betterNodeFinder->findInstanceOf($stmts, StaticCall::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, $staticCalls): void { + foreach ($staticCalls as $staticCall) { + $rector->refactor($staticCall); + } +}; + +// 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($staticCalls); + +printf('static calls: %d, repeats: %d, refactor() calls: %d%s', count($staticCalls), 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); diff --git a/rules-tests/Php74/Rector/StaticCall/ExportToReflectionFunctionRector/Fixture/skip_other_class.php.inc b/rules-tests/Php74/Rector/StaticCall/ExportToReflectionFunctionRector/Fixture/skip_other_class.php.inc new file mode 100644 index 00000000000..18f6ee44347 --- /dev/null +++ b/rules-tests/Php74/Rector/StaticCall/ExportToReflectionFunctionRector/Fixture/skip_other_class.php.inc @@ -0,0 +1,18 @@ +getType($node->class); + $callerType = $this->resolveCallerType($node->class); if (! $callerType->isSuperTypeOf(new ObjectType('ReflectionFunction'))->yes()) { return null; } @@ -101,4 +103,21 @@ public function refactor(Node $node): ?Node return $new; } + + private function resolveCallerType(Name $name): Type + { + // a concrete class name is resolvable straight from the node, no type inference needed; + // self/static/parent depend on the enclosing class, so fall back to scope-based type + $className = $this->getName($name); + if ($className === null) { + return $this->getType($name); + } + + $loweredClassName = strtolower($className); + if (in_array($loweredClassName, [ObjectReference::SELF, ObjectReference::STATIC, ObjectReference::PARENT], true)) { + return $this->getType($name); + } + + return new ObjectType($className); + } }