diff --git a/bench/call_like_reflection_visitor_bench.php b/bench/call_like_reflection_visitor_bench.php new file mode 100644 index 00000000000..3c969f724d4 --- /dev/null +++ b/bench/call_like_reflection_visitor_bench.php @@ -0,0 +1,102 @@ +foo($x, $y) - the overwhelming majority - paid for the resolve + * and then decorated nothing. + * + * The optimization adds a cheap node-only pre-check (hasClosureArrowOrArrayArg): if no argument + * is a Closure/ArrowFunction/Array_, enterNode bails before resolveFunctionLikeReflectionFromCall. + * + * This bench parses a file full of ordinary calls (scalar/variable args only), attaches scope, + * then drives enterNode() over every CallLike node. Those calls decorate nothing, so the calls + * are repeatable. + * + * Run on both branches to compare: + * php bench/call_like_reflection_visitor_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\CallLike; +use Rector\DependencyInjection\LazyContainerFactory; +use Rector\PhpParser\Node\BetterNodeFinder; +use Rector\PhpParser\NodeVisitor\CallLikeReflectionNodeVisitor; +use Rector\Testing\TestingParser\TestingParser; + +require __DIR__ . '/../vendor/autoload.php'; + +const METHODS = 60; // methods, each with a handful of ordinary calls +const REPEAT = 40; // 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); + +$visitor = $rectorConfig->make(CallLikeReflectionNodeVisitor::class); + +// a class with many methods that make ordinary calls: scalar/variable args, no closures/arrays +$source = "helper(\$value, \$length);\n"; + $source .= " return \$exists;\n"; + $source .= " }\n\n"; +} +$source .= " public function helper(string \$value, int \$length)\n {\n }\n}\n"; + +$sourceFilePath = tempnam(sys_get_temp_dir(), 'bench_') . '.php'; +file_put_contents($sourceFilePath, $source); + +$stmts = $testingParser->parseFileToDecoratedNodes($sourceFilePath); + +/** @var CallLike[] $callLikes */ +$callLikes = $betterNodeFinder->findInstanceOf($stmts, CallLike::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 ($visitor, $callLikes): void { + foreach ($callLikes as $callLike) { + $visitor->enterNode($callLike); + } +}; + +// warm up 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($callLikes); + +printf('CallLike nodes: %d, repeats: %d, enterNode() calls: %d%s', count($callLikes), 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/src/PhpParser/NodeVisitor/CallLikeReflectionNodeVisitor.php b/src/PhpParser/NodeVisitor/CallLikeReflectionNodeVisitor.php index d7782ca763f..f7e479eee72 100644 --- a/src/PhpParser/NodeVisitor/CallLikeReflectionNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/CallLikeReflectionNodeVisitor.php @@ -53,6 +53,11 @@ public function enterNode(Node $node): ?Node return null; } + // only closure/arrow-fn or array args are decorated below; skip the reflection otherwise + if (! $this->hasClosureArrowOrArrayArg($args)) { + return null; + } + $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); $this->decorateClosureWithVariadicArgs($args, $functionLikeReflection); @@ -61,6 +66,19 @@ public function enterNode(Node $node): ?Node return null; } + /** + * @param Arg[] $args + */ + private function hasClosureArrowOrArrayArg(array $args): bool + { + return array_any( + $args, + static fn (Arg $arg): bool => $arg->value instanceof Closure + || $arg->value instanceof ArrowFunction + || $arg->value instanceof Array_ + ); + } + /** * @param Arg[] $args */