From c8d06394678ed7235a808e0e75599c685e545447 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 20 Oct 2026 22:09:21 +0200 Subject: [PATCH] [perf] Bucket generic tags by name in AnnotationToAttributeRector processGenericTags() scanned the whole configured annotation list for every generic doc tag, running strtolower() on both sides per entry. Bucket the list by lowercased tag once in configure() and look the tag up directly. The doctrine-annotation path (matchAnnotationToAttribute, hasClassName) keeps the flat list, as it matches by resolved class name, not tag string. bench/annotation_to_attribute_generic_tag_bench.php (400 entries, 100 generic-tag methods): 259.0 -> 20.8 us/call. --- ...otation_to_attribute_generic_tag_bench.php | 102 ++++++++++++++++++ .../Class_/AnnotationToAttributeRector.php | 18 ++-- 2 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 bench/annotation_to_attribute_generic_tag_bench.php diff --git a/bench/annotation_to_attribute_generic_tag_bench.php b/bench/annotation_to_attribute_generic_tag_bench.php new file mode 100644 index 00000000000..26cf6072a14 --- /dev/null +++ b/bench/annotation_to_attribute_generic_tag_bench.php @@ -0,0 +1,102 @@ +create(); +$rectorConfig->boot(); + +/** @var TestingParser $testingParser */ +$testingParser = $rectorConfig->make(TestingParser::class); + +/** @var BetterNodeFinder $betterNodeFinder */ +$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class); + +$rector = $rectorConfig->make(AnnotationToAttributeRector::class); + +$annotationsToAttributes = []; +for ($i = 0; $i < ENTRIES; ++$i) { + $annotationsToAttributes[] = new AnnotationToAttribute('Bench\\SomeAnnotation' . $i); +} + +$rector->configure($annotationsToAttributes); + +$source = "parseFileToDecoratedNodes($sourceFilePath); + +/** @var ClassMethod[] $classMethods */ +$classMethods = $betterNodeFinder->findInstanceOf($stmts, ClassMethod::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, $classMethods): void { + foreach ($classMethods as $classMethod) { + $rector->refactor($classMethod); + } +}; + +// 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($classMethods); + +printf('entries: %d, methods: %d, repeats: %d, refactor() calls: %d%s', ENTRIES, count($classMethods), 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/Php80/Rector/Class_/AnnotationToAttributeRector.php b/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php index f166ac63e68..e5e7c0fb3d8 100644 --- a/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php +++ b/rules/Php80/Rector/Class_/AnnotationToAttributeRector.php @@ -63,6 +63,11 @@ final class AnnotationToAttributeRector extends AbstractRector implements Config */ private array $annotationsToAttributes = []; + /** + * @var array + */ + private array $annotationsToAttributesByTag = []; + public function __construct( private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory, private readonly AttrGroupsFactory $attrGroupsFactory, @@ -178,6 +183,12 @@ public function configure(array $configuration): void Assert::allIsAOf($configuration, AnnotationToAttribute::class); $this->annotationsToAttributes = $this->resolveWithChangedAttributesClass($configuration); + + // bucket by lowercased tag for the generic-tag path + $this->annotationsToAttributesByTag = []; + foreach ($this->annotationsToAttributes as $annotationToAttribute) { + $this->annotationsToAttributesByTag[strtolower($annotationToAttribute->getTag())][] = $annotationToAttribute; + } } public function provideMinPhpVersion(): int @@ -234,12 +245,7 @@ private function processGenericTags(PhpDocInfo $phpDocInfo): array return null; } - foreach ($this->annotationsToAttributes as $annotationToAttribute) { - $desiredTag = $annotationToAttribute->getTag(); - if (strtolower($desiredTag) !== strtolower($tag)) { - continue; - } - + foreach ($this->annotationsToAttributesByTag[strtolower($tag)] ?? [] as $annotationToAttribute) { // make sure the attribute class really exists to avoid error on early upgrade if (! $this->reflectionProvider->hasClass($annotationToAttribute->getAttributeClass())) { continue;