Skip to content

Commit be841cd

Browse files
authored
[perf] Bucket generic tags by name in AnnotationToAttributeRector (#8745)
1 parent 39deace commit be841cd

2 files changed

Lines changed: 114 additions & 6 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Benchmark for AnnotationToAttributeRector::processGenericTags().
7+
*
8+
* For every generic doc tag it scans the whole configured annotation list, comparing
9+
* strtolower($desiredTag) with the tag name. Upgrade sets configure many entries, so this is a
10+
* per-tag linear scan.
11+
*
12+
* This bench drives refactor() over many class methods whose docblock holds a single generic tag
13+
* that is NOT in the config (the common case). A bare generic tag is not a DoctrineAnnotationTagValueNode,
14+
* so the processDoctrineAnnotationClasses()/matchAnnotationToAttribute() path is skipped and only the
15+
* processGenericTags() scan is exercised.
16+
*
17+
* Run on both branches to compare:
18+
* php bench/annotation_to_attribute_generic_tag_bench.php
19+
*
20+
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy under
21+
* concurrent load.
22+
*/
23+
24+
use PhpParser\Node\Stmt\ClassMethod;
25+
use Rector\DependencyInjection\LazyContainerFactory;
26+
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
27+
use Rector\Php80\ValueObject\AnnotationToAttribute;
28+
use Rector\PhpParser\Node\BetterNodeFinder;
29+
use Rector\Testing\TestingParser\TestingParser;
30+
31+
require __DIR__ . '/../vendor/autoload.php';
32+
33+
const ENTRIES = 400; // configured annotation-to-attribute entries
34+
const METHODS = 100; // class methods with a generic doc tag per pass
35+
const REPEAT = 200; // outer repeats over the whole set
36+
37+
$rectorConfig = (new LazyContainerFactory())->create();
38+
$rectorConfig->boot();
39+
40+
/** @var TestingParser $testingParser */
41+
$testingParser = $rectorConfig->make(TestingParser::class);
42+
43+
/** @var BetterNodeFinder $betterNodeFinder */
44+
$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class);
45+
46+
$rector = $rectorConfig->make(AnnotationToAttributeRector::class);
47+
48+
$annotationsToAttributes = [];
49+
for ($i = 0; $i < ENTRIES; ++$i) {
50+
$annotationsToAttributes[] = new AnnotationToAttribute('Bench\\SomeAnnotation' . $i);
51+
}
52+
53+
$rector->configure($annotationsToAttributes);
54+
55+
$source = "<?php\n\nnamespace Bench;\n\nclass SomeClass\n{\n";
56+
for ($i = 0; $i < METHODS; ++$i) {
57+
// a bare generic tag deliberately absent from the configured entries
58+
$source .= " /**\n * @freshTag{$i}\n */\n public function method{$i}()\n {\n }\n\n";
59+
}
60+
61+
$source .= "}\n";
62+
63+
$sourceFilePath = tempnam(sys_get_temp_dir(), 'bench_') . '.php';
64+
file_put_contents($sourceFilePath, $source);
65+
66+
$stmts = $testingParser->parseFileToDecoratedNodes($sourceFilePath);
67+
68+
/** @var ClassMethod[] $classMethods */
69+
$classMethods = $betterNodeFinder->findInstanceOf($stmts, ClassMethod::class);
70+
71+
$cpuSeconds = static function (): float {
72+
$usage = getrusage();
73+
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
74+
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
75+
};
76+
77+
$run = static function () use ($rector, $classMethods): void {
78+
foreach ($classMethods as $classMethod) {
79+
$rector->refactor($classMethod);
80+
}
81+
};
82+
83+
// warm up autoloading/reflection caches once, excluded from timing
84+
$run();
85+
86+
$wallStart = microtime(true);
87+
$cpuStart = $cpuSeconds();
88+
89+
for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
90+
$run();
91+
}
92+
93+
$cpu = $cpuSeconds() - $cpuStart;
94+
$wall = microtime(true) - $wallStart;
95+
96+
@unlink($sourceFilePath);
97+
98+
$totalCalls = REPEAT * count($classMethods);
99+
100+
printf('entries: %d, methods: %d, repeats: %d, refactor() calls: %d%s', ENTRIES, count($classMethods), REPEAT, $totalCalls, PHP_EOL);
101+
printf('CPU: %.4f s (%.3f us/call)%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL);
102+
printf('wall: %.4f s%s', $wall, PHP_EOL);

‎rules/Php80/Rector/Class_/AnnotationToAttributeRector.php‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ final class AnnotationToAttributeRector extends AbstractRector implements Config
6363
*/
6464
private array $annotationsToAttributes = [];
6565

66+
/**
67+
* @var array<string, AnnotationToAttribute[]>
68+
*/
69+
private array $annotationsToAttributesByTag = [];
70+
6671
public function __construct(
6772
private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
6873
private readonly AttrGroupsFactory $attrGroupsFactory,
@@ -178,6 +183,12 @@ public function configure(array $configuration): void
178183
Assert::allIsAOf($configuration, AnnotationToAttribute::class);
179184

180185
$this->annotationsToAttributes = $this->resolveWithChangedAttributesClass($configuration);
186+
187+
// bucket by lowercased tag for the generic-tag path
188+
$this->annotationsToAttributesByTag = [];
189+
foreach ($this->annotationsToAttributes as $annotationToAttribute) {
190+
$this->annotationsToAttributesByTag[strtolower($annotationToAttribute->getTag())][] = $annotationToAttribute;
191+
}
181192
}
182193

183194
public function provideMinPhpVersion(): int
@@ -234,12 +245,7 @@ private function processGenericTags(PhpDocInfo $phpDocInfo): array
234245
return null;
235246
}
236247

237-
foreach ($this->annotationsToAttributes as $annotationToAttribute) {
238-
$desiredTag = $annotationToAttribute->getTag();
239-
if (strtolower($desiredTag) !== strtolower($tag)) {
240-
continue;
241-
}
242-
248+
foreach ($this->annotationsToAttributesByTag[strtolower($tag)] ?? [] as $annotationToAttribute) {
243249
// make sure the attribute class really exists to avoid error on early upgrade
244250
if (! $this->reflectionProvider->hasClass($annotationToAttribute->getAttributeClass())) {
245251
continue;

0 commit comments

Comments
 (0)