Skip to content

Commit

Permalink
[TwigComponent][WebProfiler] Add profile + StopWatch + WDT
Browse files Browse the repository at this point in the history
  • Loading branch information
smnandre authored and weaverryan committed Oct 3, 2023
1 parent 1fcb41f commit 28baab1
Show file tree
Hide file tree
Showing 14 changed files with 869 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/TwigComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 2.13.0

- Add profiler integration: `TwigComponentDataCollector` and debug toolbar templates

## 2.12.0

- Added a `debug:twig-component` command.
Expand Down
1 change: 1 addition & 0 deletions src/TwigComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"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"
},
Expand Down
39 changes: 39 additions & 0 deletions src/TwigComponent/config/debug.php
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,
]);
};
2 changes: 1 addition & 1 deletion src/TwigComponent/src/ComponentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function render(MountedComponent $mounted): string
$event->getTemplateIndex(),
)->render($event->getVariables());
} finally {
$this->componentStack->pop();
$mounted = $this->componentStack->pop();

$event = new PostRenderEvent($mounted);
$this->dispatcher->dispatch($event);
Expand Down
186 changes: 186 additions & 0 deletions src/TwigComponent/src/DataCollector/TwigComponentDataCollector.php
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@

namespace Symfony\UX\TwigComponent\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Command\ComponentDebugCommand;
use Symfony\UX\TwigComponent\ComponentFactory;
Expand All @@ -41,6 +44,8 @@ final class TwigComponentExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));

if (!isset($container->getParameter('kernel.bundles')['TwigBundle'])) {
throw new LogicException('The TwigBundle is not registered in your application. Try running "composer require symfony/twig-bundle".');
}
Expand Down Expand Up @@ -105,5 +110,9 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
])
->addTag('console.command')
;

if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class)) {
$loader->load('debug.php');
}
}
}
101 changes: 101 additions & 0 deletions src/TwigComponent/src/EventListener/TwigComponentLoggerListener.php
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 = [];
}
}
4 changes: 4 additions & 0 deletions src/TwigComponent/templates/Collector/chevron-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 28baab1

Please sign in to comment.