-
-
Notifications
You must be signed in to change notification settings - Fork 329
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TwigComponent][WebProfiler] Add profile + StopWatch + WDT
- Loading branch information
1 parent
1fcb41f
commit 28baab1
Showing
14 changed files
with
869 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\TwigComponent\DependencyInjection\Loader\Configurator; | ||
|
||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; | ||
use Symfony\UX\TwigComponent\DataCollector\TwigComponentDataCollector; | ||
use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; | ||
|
||
use function Symfony\Component\DependencyInjection\Loader\Configurator\service; | ||
|
||
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) | ||
->args([ | ||
service('ux.twig_component.component_logger_listener'), | ||
service('twig'), | ||
]) | ||
->tag('data_collector', [ | ||
'template' => '@TwigComponent/Collector/twig_component.html.twig', | ||
'id' => 'twig_component', | ||
'priority' => 256, | ||
]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\UX\TwigComponent\DataCollector; | ||
|
||
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; | ||
use Symfony\Component\VarDumper\Caster\ClassStub; | ||
use Symfony\Component\VarDumper\Cloner\Data; | ||
use Symfony\UX\TwigComponent\Event\PostRenderEvent; | ||
use Symfony\UX\TwigComponent\Event\PreRenderEvent; | ||
use Symfony\UX\TwigComponent\EventListener\TwigComponentLoggerListener; | ||
use Twig\Environment; | ||
use Twig\Error\LoaderError; | ||
|
||
/** | ||
* @author Simon André <[email protected]> | ||
*/ | ||
class TwigComponentDataCollector extends AbstractDataCollector implements LateDataCollectorInterface | ||
{ | ||
private bool $hasStub; | ||
|
||
public function __construct( | ||
private readonly TwigComponentLoggerListener $logger, | ||
private readonly Environment $twig, | ||
) { | ||
$this->hasStub = class_exists(ClassStub::class); | ||
} | ||
|
||
public function collect(Request $request, Response $response, \Throwable $exception = null): void | ||
{ | ||
} | ||
|
||
public function lateCollect(): void | ||
{ | ||
$this->collectDataFromLogger(); | ||
$this->data = $this->cloneVar($this->data); | ||
} | ||
|
||
public function getData(): array|Data | ||
{ | ||
return $this->data; | ||
} | ||
|
||
public function getName(): string | ||
{ | ||
return 'twig_component'; | ||
} | ||
|
||
public function reset(): void | ||
{ | ||
$this->logger->reset(); | ||
parent::reset(); | ||
} | ||
|
||
public function getComponents(): array|Data | ||
{ | ||
return $this->data['components'] ?? []; | ||
} | ||
|
||
public function getComponentCount(): int | ||
{ | ||
return $this->data['component_count'] ?? 0; | ||
} | ||
|
||
public function getPeakMemoryUsage(): int | ||
{ | ||
return $this->data['peak_memory_usage'] ?? 0; | ||
} | ||
|
||
public function getRenders(): array|Data | ||
{ | ||
return $this->data['renders'] ?? []; | ||
} | ||
|
||
public function getRenderCount(): int | ||
{ | ||
return $this->data['render_count'] ?? 0; | ||
} | ||
|
||
public function getRenderTime(): int | ||
{ | ||
return $this->data['render_time'] ?? 0; | ||
} | ||
|
||
private function collectDataFromLogger(): void | ||
{ | ||
$components = []; | ||
$renders = []; | ||
$ongoingRenders = []; | ||
|
||
foreach ($this->logger->getEvents() as [$event, $profile]) { | ||
if ($event instanceof PreRenderEvent) { | ||
$mountedComponent = $event->getMountedComponent(); | ||
|
||
$metadata = $event->getMetadata(); | ||
$componentName = $metadata->getName(); | ||
$componentClass = $mountedComponent->getComponent()::class; | ||
|
||
$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 ? | ||
'render_count' => 0, | ||
'render_time' => 0, | ||
]; | ||
|
||
$renderId = spl_object_id($mountedComponent); | ||
$renders[$renderId] = [ | ||
'name' => $componentName, | ||
'class' => $componentClass, | ||
'is_embed' => $event->isEmbedded(), | ||
'input_props' => $mountedComponent->getInputProps(), | ||
'attributes' => $mountedComponent->getAttributes()->all(), | ||
'variables' => $event->getVariables(), | ||
'template_index' => $event->getTemplateIndex(), | ||
'component' => $mountedComponent->getComponent(), | ||
'depth' => \count($ongoingRenders), | ||
'children' => [], | ||
'render_start' => $profile[0], | ||
]; | ||
|
||
if ($parentId = end($ongoingRenders)) { | ||
$renders[$parentId]['children'][] = $renderId; | ||
} | ||
|
||
$ongoingRenders[$renderId] = $renderId; | ||
continue; | ||
} | ||
|
||
if ($event instanceof PostRenderEvent) { | ||
$mountedComponent = $event->getMountedComponent(); | ||
$componentName = $mountedComponent->getName(); | ||
$renderId = spl_object_id($mountedComponent); | ||
|
||
$renderTime = ($profile[0] - $renders[$renderId]['render_start']) * 1000; | ||
$renders[$renderId] += [ | ||
'render_end' => $profile[0], | ||
'render_time' => $renderTime, | ||
'render_memory' => $profile[1], | ||
]; | ||
|
||
++$components[$componentName]['render_count']; | ||
$components[$componentName]['render_time'] += $renderTime; | ||
|
||
unset($ongoingRenders[$renderId]); | ||
} | ||
} | ||
|
||
// Sort by render count DESC | ||
uasort($components, fn ($a, $b) => $b['render_count'] <=> $a['render_count']); | ||
|
||
$this->data['components'] = $components; | ||
$this->data['component_count'] = \count($components); | ||
|
||
$this->data['renders'] = $renders; | ||
$this->data['render_count'] = \count($renders); | ||
$rootRenders = array_filter($renders, fn (array $r) => 0 === $r['depth']); | ||
$this->data['render_time'] = array_sum(array_column($rootRenders, 'render_time')); | ||
|
||
$this->data['peak_memory_usage'] = max([0, ...array_column($renders, 'render_memory')]); | ||
} | ||
|
||
private function resolveTemplatePath(string $logicalName): ?string | ||
{ | ||
try { | ||
$source = $this->twig->getLoader()->getSourceContext($logicalName); | ||
} catch (LoaderError) { | ||
return null; | ||
} | ||
|
||
return $source->getPath(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
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é <[email protected]> | ||
*/ | ||
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], | ||
]; | ||
} | ||
|
||
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); | ||
} | ||
|
||
public function onPostRender(PostRenderEvent $event): void | ||
{ | ||
if ($this->stopwatch?->isStarted($name = $event->getMountedComponent()->getName())) { | ||
$this->stopwatch->stop($name); | ||
} | ||
$this->logEvent($event); | ||
} | ||
|
||
public function reset(): void | ||
{ | ||
$this->events = []; | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.