Skip to content

Commit 48bed26

Browse files
committed
[TwigComponent] Cache attribute-method reflection lookups per class
| Q | A | -------------- | --- | Bug fix? | no | New feature? | no | Deprecations? | no | Documentation? | no | Issues | - | License | MIT `AsTwigComponent::attributeMethodsFor()` rebuilt a `ReflectionClass` and scanned every public method on each call. LiveComponent calls it five times per component per request (`liveListeners()`, `preDehydrateMethods()`, `postHydrateMethods()`, `preReRenderMethods()`, `isActionAllowed()`), so a page with several live components re-ran the same reflection scan dozens of times. Memoize the resolved methods per class and attribute. The results only depend on the class, so the cache is valid for the whole process. `attributeMethodsFor()` returns the cached array directly and widens its return type from `\Traversable` to `iterable`; both callers only iterate it. 20k lookups on a component with 13 public methods: ~35 ms -> ~5 ms. Benchmarked through LiveComponent, where the callers live. LiveComponent installs `symfony/ux-twig-component` from Packagist rather than from the monorepo, so copy the patched file over `src/LiveComponent/vendor/symfony/ux-twig-component/src/Attribute/AsTwigComponent.php` first, then run from the repository root with `blackfire run symfony php bench.php`: ```php <?php require __DIR__.'/src/LiveComponent/vendor/autoload.php'; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\PostHydrate; use Symfony\UX\LiveComponent\Attribute\PreDehydrate; use Symfony\UX\LiveComponent\Attribute\PreReRender; #[AsLiveComponent('Bench')] class BenchComponent { public string $a = ''; public string $b = ''; public string $c = ''; #[PreDehydrate] public function onPreDehydrate() {} #[PostHydrate] public function onPostHydrate() {} #[PreReRender] public function onPreReRender() {} #[LiveAction] public function save() {} #[LiveListener('foo:bar')] public function onFooBar() {} public function noise1() {} public function noise2() {} public function noise3() {} public function noise4() {} public function noise5() {} public function noise6() {} public function noise7() {} public function noise8() {} } $component = new BenchComponent(); // Mimics 20 requests for a page holding 20 live components. for ($i = 0; $i < 20 * 20; ++$i) { AsLiveComponent::liveListeners($component); AsLiveComponent::preDehydrateMethods($component); AsLiveComponent::postHydrateMethods($component); AsLiveComponent::preReRenderMethods($component); AsLiveComponent::isActionAllowed($component, 'save'); } ``` Blackfire: - before — 78ms wall / 69ms CPU: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/407f4e37-61aa-4dba-bacb-af56a3b2bde9/graph - after — 20ms wall / 20ms CPU: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/c91ba592-76e1-4a08-a920-43bf67989efd/graph - diff: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/407f4e37-61aa-4dba-bacb-af56a3b2bde9...c91ba592-76e1-4a08-a920-43bf67989efd/graph Analysis, implementation and benchmarks by Claude Opus 5.
1 parent 26a3746 commit 48bed26

1 file changed

Lines changed: 46 additions & 6 deletions

File tree

src/TwigComponent/src/Attribute/AsTwigComponent.php

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@
2121
#[\Attribute(\Attribute::TARGET_CLASS)]
2222
class AsTwigComponent
2323
{
24+
/**
25+
* Declaration order, as returned by reflection.
26+
*
27+
* @var array<class-string, array<class-string, \ReflectionMethod[]>>
28+
*/
29+
private static array $attributeMethods = [];
30+
31+
/**
32+
* Same methods, sorted by descending attribute priority.
33+
*
34+
* @var array<class-string, array<class-string, \ReflectionMethod[]>>
35+
*/
36+
private static array $attributeMethodsByPriority = [];
37+
2438
public function __construct(
2539
/**
2640
* The component name (ie: Button).
@@ -94,26 +108,52 @@ public function serviceConfig(): array
94108
*/
95109
protected static function attributeMethodsByPriorityFor(object|string $component, string $attributeClass): array
96110
{
97-
$methods = iterator_to_array(self::attributeMethodsFor($attributeClass, $component));
111+
$class = \is_object($component) ? $component::class : $component;
112+
113+
if (isset(self::$attributeMethodsByPriority[$class][$attributeClass])) {
114+
return self::$attributeMethodsByPriority[$class][$attributeClass];
115+
}
116+
117+
$methods = self::findAttributeMethods($attributeClass, $class);
98118

99119
usort($methods, static function (\ReflectionMethod $a, \ReflectionMethod $b) use ($attributeClass) {
100120
return $a->getAttributes($attributeClass)[0]->newInstance()->priority <=> $b->getAttributes($attributeClass)[0]->newInstance()->priority;
101121
});
102122

103-
return array_reverse($methods);
123+
return self::$attributeMethodsByPriority[$class][$attributeClass] = array_reverse($methods);
104124
}
105125

106126
/**
107-
* @return \Traversable<\ReflectionMethod>
127+
* @return iterable<\ReflectionMethod>
108128
*
109129
* @internal
110130
*/
111-
protected static function attributeMethodsFor(string $attribute, object|string $component): \Traversable
131+
protected static function attributeMethodsFor(string $attribute, object|string $component): iterable
112132
{
113-
foreach (new \ReflectionClass($component)->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
133+
return self::findAttributeMethods($attribute, $component);
134+
}
135+
136+
/**
137+
* @param class-string $attribute
138+
* @param object|class-string $component
139+
*
140+
* @return \ReflectionMethod[]
141+
*/
142+
private static function findAttributeMethods(string $attribute, object|string $component): array
143+
{
144+
$class = \is_object($component) ? $component::class : $component;
145+
146+
if (isset(self::$attributeMethods[$class][$attribute])) {
147+
return self::$attributeMethods[$class][$attribute];
148+
}
149+
150+
$methods = [];
151+
foreach (new \ReflectionClass($class)->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
114152
if ($method->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
115-
yield $method;
153+
$methods[] = $method;
116154
}
117155
}
156+
157+
return self::$attributeMethods[$class][$attribute] = $methods;
118158
}
119159
}

0 commit comments

Comments
 (0)