Repository: symfony/ux
Pull request #2167 (2024-09-15 01:06:52)

[TwigComponent] Optimize the ComponentLoggerListener
* Clean unused code
* Reduce memory usage
* Optimize class/templates lookup

javiereguiluz: Nice! Maybe we can also remove the `symfony/stopwatch` dev dependency here?

https://github.com/symfony/ux/blob/8dd706da2c1ada2174c920c246daa6765b176cef/src/TwigComponent/composer.json#L43C18-L43C27

Patch:
diff --git a/src/TwigComponent/composer.json b/src/TwigComponent/composer.json
index 77f5a324..1e0ca5d3 100644
--- a/src/TwigComponent/composer.json
+++ b/src/TwigComponent/composer.json
@@ -40,7 +40,6 @@
         "symfony/framework-bundle": "^5.4|^6.0|^7.0",
         "symfony/phpunit-bridge": "^6.0|^7.0",
         "symfony/stimulus-bundle": "^2.9.1",
-        "symfony/stopwatch": "^5.4|^6.0|^7.0",
         "symfony/twig-bundle": "^5.4|^6.0|^7.0",
         "symfony/webpack-encore-bundle": "^1.15"
     },
diff --git a/src/TwigComponent/config/debug.php b/src/TwigComponent/config/debug.php
index 6fb748d9..6afabb82 100644
--- a/src/TwigComponent/config/debug.php
+++ b/src/TwigComponent/config/debug.php
@@ -21,9 +21,6 @@ return static function (ContainerConfigurator $container) {
     $container->services()
 
         ->set('ux.twig_component.component_logger_listener', TwigComponentLoggerListener::class)
-        ->args([
-            service('debug.stopwatch')->ignoreOnInvalid(),
-        ])
         ->tag('kernel.event_subscriber')
 
         ->set('ux.twig_component.data_collector', TwigComponentDataCollector::class)
diff --git a/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php b/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
index f0c1e05f..1ee94c71 100644
--- a/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
+++ b/src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
@@ -25,8 +25,10 @@ use Twig\Error\LoaderError;
 
 /**
  * @author Simon André <smn.andre@gmail.com>
+ *
+ * @internal
  */
-class TwigComponentDataCollector extends AbstractDataCollector implements LateDataCollectorInterface
+final class TwigComponentDataCollector extends AbstractDataCollector implements LateDataCollectorInterface
 {
     private bool $hasStub;
 
@@ -99,6 +101,9 @@ class TwigComponentDataCollector extends AbstractDataCollector implements LateDa
         $renders = [];
         $ongoingRenders = [];
 
+        $classStubs = [];
+        $templatePaths = [];
+
         foreach ($this->logger->getEvents() as [$event, $profile]) {
             if ($event instanceof PreRenderEvent) {
                 $mountedComponent = $event->getMountedComponent();
@@ -110,9 +115,9 @@ class TwigComponentDataCollector extends AbstractDataCollector implements LateDa
                 $components[$componentName] ??= [
                     'name' => $componentName,
                     'class' => $componentClass,
-                    'class_stub' => $this->hasStub ? new ClassStub($componentClass) : $componentClass,
-                    'template' => $metadata->getTemplate(),
-                    'template_path' => $this->resolveTemplatePath($metadata->getTemplate()), // defer ? lazy ?
+                    'class_stub' => $classStubs[$componentClass] ??= ($this->hasStub ? new ClassStub($componentClass) : $componentClass),
+                    'template' => $template = $metadata->getTemplate(),
+                    'template_path' => $templatePaths[$template] ??= $this->resolveTemplatePath($template),
                     'render_count' => 0,
                     'render_time' => 0,
                 ];
diff --git a/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php b/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php
index acabac5c..62af18d3 100644
--- a/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php
+++ b/src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php
@@ -12,75 +12,38 @@
 namespace Symfony\UX\TwigComponent\EventListener;
 
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
-use Symfony\Component\Stopwatch\Stopwatch;
 use Symfony\Contracts\Service\ResetInterface;
-use Symfony\UX\TwigComponent\Event\PostMountEvent;
 use Symfony\UX\TwigComponent\Event\PostRenderEvent;
-use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
-use Symfony\UX\TwigComponent\Event\PreMountEvent;
 use Symfony\UX\TwigComponent\Event\PreRenderEvent;
 
 /**
  * @author Simon André <smn.andre@gmail.com>
+ *
+ * @internal
  */
-class TwigComponentLoggerListener implements EventSubscriberInterface, ResetInterface
+final class TwigComponentLoggerListener implements EventSubscriberInterface, ResetInterface
 {
     private array $events = [];
 
-    public function __construct(private ?Stopwatch $stopwatch = null)
-    {
-    }
-
     public static function getSubscribedEvents(): array
     {
         return [
-            PreCreateForRenderEvent::class => [
-                // High priority: start the stopwatch as soon as possible
-                ['onPreCreateForRender', 255],
-                // Low priority: check `event::getRenderedString()` as late as possible
-                ['onPostCreateForRender', -255],
-            ],
-            PreMountEvent::class => ['onPreMount', 255],
-            PostMountEvent::class => ['onPostMount', -255],
             PreRenderEvent::class => ['onPreRender', 255],
             PostRenderEvent::class => ['onPostRender', -255],
         ];
     }
 
+    /**
+     * @return list<array{
+     *     PreRenderEvent|PostRenderEvent,
+     *     array{float, int},
+     * }>
+     */
     public function getEvents(): array
     {
         return $this->events;
     }
 
-    public function onPreCreateForRender(PreCreateForRenderEvent $event): void
-    {
-        $this->stopwatch?->start($event->getName(), 'twig_component');
-        $this->logEvent($event);
-    }
-
-    private function logEvent(object $event): void
-    {
-        $this->events[] = [$event, [microtime(true), memory_get_usage(true)]];
-    }
-
-    public function onPostCreateForRender(PreCreateForRenderEvent $event): void
-    {
-        if (\is_string($event->getRenderedString())) {
-            $this->stopwatch?->stop($event->getName());
-            $this->logEvent($event);
-        }
-    }
-
-    public function onPreMount(PreMountEvent $event): void
-    {
-        $this->logEvent($event);
-    }
-
-    public function onPostMount(PostMountEvent $event): void
-    {
-        $this->logEvent($event);
-    }
-
     public function onPreRender(PreRenderEvent $event): void
     {
         $this->logEvent($event);
@@ -88,9 +51,6 @@ class TwigComponentLoggerListener implements EventSubscriberInterface, ResetInte
 
     public function onPostRender(PostRenderEvent $event): void
     {
-        if ($this->stopwatch?->isStarted($name = $event->getMountedComponent()->getName())) {
-            $this->stopwatch->stop($name);
-        }
         $this->logEvent($event);
     }
 
@@ -98,4 +58,9 @@ class TwigComponentLoggerListener implements EventSubscriberInterface, ResetInte
     {
         $this->events = [];
     }
+
+    private function logEvent(object $event): void
+    {
+        $this->events[] = [$event, [microtime(true), memory_get_usage(true)]];
+    }
 }


Test patch:
diff --git a/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php b/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php
index df6b81c7..cd5ab7a9 100644
--- a/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php
+++ b/src/TwigComponent/tests/Unit/EventListener/TwigComponentLoggerListenerTest.php
@@ -14,10 +14,7 @@ namespace Symfony\UX\TwigComponent\Tests\Unit\EventListener;
 use PHPUnit\Framework\TestCase;
 use Symfony\UX\TwigComponent\ComponentAttributes;
 use Symfony\UX\TwigComponent\ComponentMetadata;
-use Symfony\UX\TwigComponent\Event\PostMountEvent;
 use Symfony\UX\TwigComponent\Event\PostRenderEvent;
-use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;
-use Symfony\UX\TwigComponent\Event\PreMountEvent;
 use Symfony\UX\TwigComponent\Event\PreRenderEvent;
 use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener;
 use Symfony\UX\TwigComponent\MountedComponent;
@@ -32,31 +29,20 @@ class TwigComponentLoggerListenerTest extends TestCase
         $logger = new TwigComponentLoggerListener();
         $this->assertSame([], $logger->getEvents());
 
-        $eventA = new PreCreateForRenderEvent('a');
-        $logger->onPreCreateForRender($eventA);
-
-        $eventB = new PreCreateForRenderEvent('b');
-        $logger->onPreCreateForRender($eventB);
-
-        $eventC = new PreMountEvent(new \stdClass(), [], new ComponentMetadata([]));
-        $logger->onPreMount($eventC);
-        $eventD = new PostMountEvent(new \stdClass(), [], new ComponentMetadata([]));
-        $logger->onPostMount($eventD);
-
         $mounted = new MountedComponent('foo', new \stdClass(), new ComponentAttributes([]));
-        $eventE = new PreRenderEvent($mounted, new ComponentMetadata(['template' => 'bar']), []);
-        $logger->onPreRender($eventE);
-        $eventF = new PostRenderEvent($mounted);
-        $logger->onPostRender($eventF);
+        $eventA = new PreRenderEvent($mounted, new ComponentMetadata(['template' => 'bar']), []);
+        $logger->onPreRender($eventA);
+        $eventB = new PostRenderEvent($mounted);
+        $logger->onPostRender($eventB);
 
-        $this->assertSame([$eventA, $eventB, $eventC, $eventD, $eventE, $eventF], array_column($logger->getEvents(), 0));
+        $this->assertSame([$eventA, $eventB], array_column($logger->getEvents(), 0));
     }
 
     public function testLoggerReset(): void
     {
         $logger = new TwigComponentLoggerListener();
 
-        $logger->onPreCreateForRender(new PreCreateForRenderEvent('foo'));
+        $logger->onPreRender(new PreRenderEvent(new MountedComponent('foo', new \stdClass(), new ComponentAttributes([])), new ComponentMetadata(['template' => 'bar']), []));
         $this->assertNotSame([], $logger->getEvents());
 
         $logger->reset();