Skip to content

Commit 62e0fcc

Browse files
authored
[perf] Only resolve call reflection for 2-item array args in CallLikeReflectionNodeVisitor (#8880)
1 parent 6f8df55 commit 62e0fcc

2 files changed

Lines changed: 118 additions & 8 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Benchmark for CallLikeReflectionNodeVisitor::enterNode() on calls with array args.
7+
*
8+
* The visitor resolved the function-like reflection for every call with an array literal arg,
9+
* to mark arrays passed to a parameter that cannot hold a Closure (IS_ARG_NOT_ACCEPTING_CLOSURE).
10+
* That attribute is only read by ArrayToFirstClassCallableRector, which bails on any array that
11+
* does not have exactly 2 items, so calls like in_array($x, ['a', 'b', 'c']) or
12+
* $this->foo(['key' => $value]) paid for the resolve and decorated nothing useful.
13+
*
14+
* The optimization narrows the node-only pre-check to 2-item arrays (the [$caller, 'method']
15+
* shape), so other array args bail before resolveFunctionLikeReflectionFromCall.
16+
*
17+
* This bench parses a file full of calls with non-callable array args, attaches scope,
18+
* then drives enterNode() over every CallLike node.
19+
*
20+
* Run on both branches to compare:
21+
* php bench/call_like_reflection_visitor_array_arg_bench.php
22+
*
23+
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy
24+
* under concurrent load.
25+
*/
26+
27+
use PhpParser\Node\Expr\CallLike;
28+
use Rector\DependencyInjection\LazyContainerFactory;
29+
use Rector\PhpParser\Node\BetterNodeFinder;
30+
use Rector\PhpParser\NodeVisitor\CallLikeReflectionNodeVisitor;
31+
use Rector\Testing\TestingParser\TestingParser;
32+
33+
require __DIR__ . '/../vendor/autoload.php';
34+
35+
const METHODS = 60; // methods, each with a handful of calls with array args
36+
const REPEAT = 40; // outer repeats over the whole call set
37+
38+
$rectorConfig = (new LazyContainerFactory())->create();
39+
$rectorConfig->boot();
40+
41+
/** @var TestingParser $testingParser */
42+
$testingParser = $rectorConfig->make(TestingParser::class);
43+
44+
/** @var BetterNodeFinder $betterNodeFinder */
45+
$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class);
46+
47+
$visitor = $rectorConfig->make(CallLikeReflectionNodeVisitor::class);
48+
49+
// a class with many methods that make calls with non-callable array args
50+
$source = "<?php\n\nnamespace Bench;\n\nclass SomeClass\n{\n";
51+
for ($i = 0; $i < METHODS; ++$i) {
52+
$source .= " public function run{$i}(string \$value, array \$items)\n {\n";
53+
$source .= " \$exists = in_array(\$value, ['a', 'b', 'c'], true);\n";
54+
$source .= " \$merged = array_merge(\$items, ['key' => \$value]);\n";
55+
$source .= " \$joined = implode(',', [\$value, \$value, \$value]);\n";
56+
$source .= " \$this->helper(\$value, []);\n";
57+
$source .= " return \$exists;\n";
58+
$source .= " }\n\n";
59+
}
60+
$source .= " public function helper(string \$value, array \$options)\n {\n }\n}\n";
61+
62+
$sourceFilePath = tempnam(sys_get_temp_dir(), 'bench_') . '.php';
63+
file_put_contents($sourceFilePath, $source);
64+
65+
$stmts = $testingParser->parseFileToDecoratedNodes($sourceFilePath);
66+
67+
/** @var CallLike[] $callLikes */
68+
$callLikes = $betterNodeFinder->findInstanceOf($stmts, CallLike::class);
69+
70+
$cpuSeconds = static function (): float {
71+
$usage = getrusage();
72+
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
73+
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
74+
};
75+
76+
$run = static function () use ($visitor, $callLikes): void {
77+
foreach ($callLikes as $callLike) {
78+
$visitor->enterNode($callLike);
79+
}
80+
};
81+
82+
// warm up caches once, excluded from timing
83+
$run();
84+
85+
$wallStart = microtime(true);
86+
$cpuStart = $cpuSeconds();
87+
88+
for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
89+
$run();
90+
}
91+
92+
$cpu = $cpuSeconds() - $cpuStart;
93+
$wall = microtime(true) - $wallStart;
94+
95+
@unlink($sourceFilePath);
96+
97+
$totalCalls = REPEAT * count($callLikes);
98+
99+
printf('CallLike nodes: %d, repeats: %d, enterNode() calls: %d%s', count($callLikes), REPEAT, $totalCalls, PHP_EOL);
100+
printf('CPU: %.4f s (%.3f us/call)%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL);
101+
printf('wall: %.4f s%s', $wall, PHP_EOL);

‎src/PhpParser/NodeVisitor/CallLikeReflectionNodeVisitor.php‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PhpParser\Node;
88
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr;
910
use PhpParser\Node\Expr\Array_;
1011
use PhpParser\Node\Expr\ArrowFunction;
1112
use PhpParser\Node\Expr\CallLike;
@@ -28,7 +29,7 @@
2829
/**
2930
* Decorates call args using the resolved function-like reflection, resolved once per call:
3031
* - Closure/arrow-fn args passed to a variadic callable parameter (HAS_CLOSURE_WITH_VARIADIC_ARGS)
31-
* - array args passed to a parameter that cannot hold a Closure, so an array callable is kept as is
32+
* - 2-item array args passed to a parameter that cannot hold a Closure, so an array callable is kept as is
3233
* (IS_ARG_NOT_ACCEPTING_CLOSURE), see https://github.com/rectorphp/rector/issues/9563
3334
*/
3435
final class CallLikeReflectionNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface
@@ -53,8 +54,8 @@ public function enterNode(Node $node): ?Node
5354
return null;
5455
}
5556

56-
// only closure/arrow-fn or array args are decorated below; skip the reflection otherwise
57-
if (! $this->hasClosureArrowOrArrayArg($args)) {
57+
// only closure/arrow-fn or array callable shaped args are decorated below; skip the reflection otherwise
58+
if (! $this->hasClosureArrowOrArrayCallableArg($args)) {
5859
return null;
5960
}
6061

@@ -69,16 +70,24 @@ public function enterNode(Node $node): ?Node
6970
/**
7071
* @param Arg[] $args
7172
*/
72-
private function hasClosureArrowOrArrayArg(array $args): bool
73+
private function hasClosureArrowOrArrayCallableArg(array $args): bool
7374
{
7475
return array_any(
7576
$args,
76-
static fn (Arg $arg): bool => $arg->value instanceof Closure
77+
fn (Arg $arg): bool => $arg->value instanceof Closure
7778
|| $arg->value instanceof ArrowFunction
78-
|| $arg->value instanceof Array_
79+
|| $this->isArrayCallableShape($arg->value)
7980
);
8081
}
8182

83+
/**
84+
* Only [$caller, 'method'] can be an array callable
85+
*/
86+
private function isArrayCallableShape(Expr $expr): bool
87+
{
88+
return $expr instanceof Array_ && count($expr->items) === 2;
89+
}
90+
8291
/**
8392
* @param Arg[] $args
8493
*/
@@ -121,7 +130,7 @@ private function decorateArgNotAcceptingClosure(
121130
return;
122131
}
123132

124-
if (! array_any($args, static fn (Arg $arg): bool => $arg->value instanceof Array_)) {
133+
if (! array_any($args, fn (Arg $arg): bool => $this->isArrayCallableShape($arg->value))) {
125134
return;
126135
}
127136

@@ -134,7 +143,7 @@ private function decorateArgNotAcceptingClosure(
134143
$closureObjectType = new ObjectType('Closure');
135144

136145
foreach ($args as $position => $arg) {
137-
if (! $arg->value instanceof Array_) {
146+
if (! $this->isArrayCallableShape($arg->value)) {
138147
continue;
139148
}
140149

0 commit comments

Comments
 (0)