Skip to content

Commit 56954f6

Browse files
authored
[perf] Compare property name before anonymous-class reflection in PropertyFetchFinder (#8712)
1 parent 65817d3 commit 56954f6

2 files changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Benchmark for the name pre-check in PropertyFetchFinder::findPropertyFetchesInClassLike().
7+
*
8+
* When scanning a class body for fetches of one property, each PropertyFetch node ran
9+
* isInAnonymous() - which calls resolveClassReflection() - before the property name was even
10+
* compared. A class body references many different properties; every fetch of an unrelated
11+
* property paid for the anonymous-class reflection lookup only to fail the name check afterwards.
12+
*
13+
* The optimization compares the property name first and bails before isInAnonymous() on a
14+
* mismatch.
15+
*
16+
* This bench parses a class whose methods reference many distinct properties (only a few match
17+
* the target name), attaches scope, then drives findPrivatePropertyFetches() for the target
18+
* property. The nodes never change, so the calls are repeatable.
19+
*
20+
* Run on both branches to compare:
21+
* php bench/property_fetch_finder_name_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\Stmt\Class_;
28+
use PhpParser\Node\Stmt\Property;
29+
use Rector\DependencyInjection\LazyContainerFactory;
30+
use Rector\PhpParser\Node\BetterNodeFinder;
31+
use Rector\PhpParser\NodeFinder\PropertyFetchFinder;
32+
use Rector\PHPStan\ScopeFetcher;
33+
use Rector\Testing\TestingParser\TestingParser;
34+
35+
require __DIR__ . '/../vendor/autoload.php';
36+
37+
const METHODS = 40; // methods, each referencing many distinct properties
38+
const FETCHES_PER_METHOD = 10; // distinct other-named property fetches per method
39+
const REPEAT = 60; // outer repeats
40+
41+
$rectorConfig = (new LazyContainerFactory())->create();
42+
$rectorConfig->boot();
43+
44+
/** @var TestingParser $testingParser */
45+
$testingParser = $rectorConfig->make(TestingParser::class);
46+
47+
/** @var BetterNodeFinder $betterNodeFinder */
48+
$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class);
49+
50+
/** @var PropertyFetchFinder $propertyFetchFinder */
51+
$propertyFetchFinder = $rectorConfig->make(PropertyFetchFinder::class);
52+
53+
// one target property, plus many methods that read unrelated properties $this->aK
54+
$source = "<?php\n\nnamespace Bench;\n\nclass SomeClass\n{\n";
55+
$source .= " private int \$target = 0;\n\n";
56+
for ($i = 0; $i < METHODS; ++$i) {
57+
$source .= " public function run{$i}()\n {\n return ";
58+
$parts = [];
59+
for ($j = 0; $j < FETCHES_PER_METHOD; ++$j) {
60+
$parts[] = "\$this->a{$j}";
61+
}
62+
$source .= implode(' + ', $parts) . ";\n }\n\n";
63+
}
64+
$source .= " public function readTarget()\n {\n return \$this->target;\n }\n}\n";
65+
66+
$sourceFilePath = tempnam(sys_get_temp_dir(), 'bench_') . '.php';
67+
file_put_contents($sourceFilePath, $source);
68+
69+
$stmts = $testingParser->parseFileToDecoratedNodes($sourceFilePath);
70+
71+
/** @var Class_ $class */
72+
$class = $betterNodeFinder->findFirstInstanceOf($stmts, Class_::class);
73+
74+
/** @var Property[] $properties */
75+
$properties = $betterNodeFinder->findInstanceOf($class, Property::class);
76+
$targetProperty = $properties[0];
77+
78+
$scope = ScopeFetcher::fetch($class);
79+
80+
$cpuSeconds = static function (): float {
81+
$usage = getrusage();
82+
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
83+
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
84+
};
85+
86+
$run = static function () use ($propertyFetchFinder, $class, $targetProperty, $scope): void {
87+
$propertyFetchFinder->findPrivatePropertyFetches($class, $targetProperty, $scope);
88+
};
89+
90+
// warm up caches once, excluded from timing
91+
$run();
92+
93+
$wallStart = microtime(true);
94+
$cpuStart = $cpuSeconds();
95+
96+
for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
97+
$run();
98+
}
99+
100+
$cpu = $cpuSeconds() - $cpuStart;
101+
$wall = microtime(true) - $wallStart;
102+
103+
@unlink($sourceFilePath);
104+
105+
printf('property fetches scanned: %d, repeats: %d%s', METHODS * FETCHES_PER_METHOD + 1, REPEAT, PHP_EOL);
106+
printf('CPU: %.4f s (%.3f ms/scan)%s', $cpu, $cpu / REPEAT * 1000, PHP_EOL);
107+
printf('wall: %.4f s%s', $wall, PHP_EOL);

‎src/PhpParser/NodeFinder/PropertyFetchFinder.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ function (Node $subNode) use ($class, $hasTrait, $propertyName, $scope): bool {
199199
}
200200

201201
if ($subNode instanceof PropertyFetch) {
202+
// cheap name mismatch bail before the anonymous-class reflection lookup
203+
if (! $this->nodeNameResolver->isName($subNode->name, $propertyName)) {
204+
return false;
205+
}
206+
202207
if ($this->isInAnonymous($subNode, $class, $hasTrait)) {
203208
return false;
204209
}

0 commit comments

Comments
 (0)