Commit 7867d2bb authored by lussoluca's avatar lussoluca

Extend ContainerAwareEventDispatcher to better trace events dispatched

parent b3183904
<?php
namespace Drupal\webprofiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Class EventPass.
*/
class EventPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
$definition = $container->findDefinition('http_kernel.basic');
$definition->replaceArgument(1, new Reference('webprofiler.debug.controller_resolver'));
// Replace the regular event_dispatcher service with the debug one.
$definition = $container->findDefinition('event_dispatcher');
$definition->setPublic(FALSE);
$container->setDefinition('webprofiler.debug.event_dispatcher.default', $definition);
$container->register('event_dispatcher', 'Drupal\webprofiler\TraceableEventDispatcher')
->addArgument(new Reference('webprofiler.debug.event_dispatcher.default'))
->addArgument(new Reference('stopwatch'))
->setProperty('_serviceId', 'event_dispatcher');
}
}
......@@ -2,74 +2,123 @@
namespace Drupal\webprofiler\DataCollector;
use Drupal\webprofiler\DrupalDataCollectorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\DataCollector\EventDataCollector as BaseEventDataCollector;
use Drupal\webprofiler\DrupalDataCollectorInterface;
use Drupal\webprofiler\EventDispatcher\EventDispatcherTraceableInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
/**
* Class EventsDataCollector
*/
class EventsDataCollector extends BaseEventDataCollector implements DrupalDataCollectorInterface {
class EventsDataCollector extends DataCollector implements DrupalDataCollectorInterface, LateDataCollectorInterface {
use StringTranslationTrait, DrupalDataCollectorTrait;
/**
* @return int
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
public function getCalledListenersCount() {
return count($this->getCalledListeners());
}
private $eventDispatcher;
/**
* @return int
* EventsDataCollector constructor.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
*/
public function getNotCalledListenersCount() {
return count($this->getNotCalledListeners());
public function __construct(EventDispatcherInterface $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public function setCalledListeners(array $listeners) {
$listeners = $this->computePriority($listeners);
$this->data['called_listeners'] = $listeners;
public function collect(Request $request, Response $response, \Exception $exception = NULL) {
$this->data = [
'called_listeners' => [],
'called_listeners_count' => 0,
'not_called_listeners' => [],
'not_called_listeners_count' => 0,
];
}
/**
* Adds the priority value to the $listeners array.
*
* @param array $listeners
* @return array
* {@inheritdoc}
*/
private function computePriority(array $listeners) {
foreach ($listeners as &$listener) {
if (is_subclass_of($listener['class'], EventSubscriberInterface::class)) {
foreach ($listener['class']::getSubscribedEvents() as $event => $methods) {
if (is_string($methods)) {
$methods = [[$methods], 0];
}
else {
if (is_string($methods[0])) {
$methods = [$methods];
}
public function lateCollect() {
if ($this->eventDispatcher instanceof EventDispatcherTraceableInterface) {
$countCalled = 0;
$calledListeners = $this->eventDispatcher->getCalledListeners();
foreach ($calledListeners as &$events) {
foreach ($events as &$priority) {
foreach ($priority as &$listener) {
$countCalled++;
$listener['clazz'] = $this->getMethodData($listener['class'], $listener['method']);
}
}
}
foreach ($methods as $method) {
if ($listener['event'] === $event) {
if ($listener['method'] === $method[0]) {
$listener['priority'] = isset($method[1]) ? $method[1] : 0;
}
}
$countNotCalled = 0;
$notCalledListeners = $this->eventDispatcher->getNotCalledListeners();
foreach ($notCalledListeners as $events) {
foreach ($events as $priority) {
foreach ($priority as $listener) {
$countNotCalled++;
}
}
} else {
$listener['priority'] = isset($listener['priority']) ? $listener['priority'] : 0;
}
$this->data = [
'called_listeners' => $calledListeners,
'called_listeners_count' => $countCalled,
'not_called_listeners' => $notCalledListeners,
'not_called_listeners_count' => $countNotCalled,
];
}
}
/**
* @return array
*/
public function getCalledListeners() {
return $this->data['called_listeners'];
}
/**
* @return array
*/
public function getNotCalledListeners() {
return $this->data['not_called_listeners'];
}
/**
* @return int
*/
public function getCalledListenersCount() {
return $this->data['called_listeners_count'];
}
return $listeners;
/**
* @return int
*/
public function getNotCalledListenersCount() {
return $this->data['not_called_listeners_count'];
}
/**
* {@inheritdoc}
*/
public function getName() {
return 'events';
}
/**
* @return mixed
*/
public function getData() {
return $this->data;
}
/**
......@@ -83,7 +132,7 @@ class EventsDataCollector extends BaseEventDataCollector implements DrupalDataCo
* {@inheritdoc}
*/
public function getPanelSummary() {
return $this->t('Called listeners: @listeners', ['@listeners' => count($this->getCalledListeners())]);
return $this->t('Called listeners: @listeners', ['@listeners' => $this->getCalledListenersCount()]);
}
/**
......
<?php
namespace Drupal\webprofiler\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
interface EventDispatcherTraceableInterface extends EventDispatcherInterface {
/**
* @return array
*/
public function getCalledListeners();
/**
* @return mixed
*/
public function getNotCalledListeners();
}
<?php
namespace Drupal\webprofiler\EventDispatcher;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\webprofiler\Stopwatch;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Class TraceableEventDispatcher
*/
class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements EventDispatcherTraceableInterface {
/**
* @var \Drupal\webprofiler\Stopwatch
* The stopwatch service.
*/
protected $stopwatch;
/**
* @var array
*/
protected $calledListeners;
/**
* @var array
*/
protected $notCalledListeners;
/**
* @param \Drupal\webprofiler\Stopwatch $stopwatch
*/
public function setStopwatch(Stopwatch $stopwatch) {
$this->stopwatch = $stopwatch;
}
/**
* {@inheritdoc}
*/
public function dispatch($event_name, Event $event = NULL) {
if ($event === NULL) {
$event = new Event();
}
$event->setDispatcher($this);
$event->setName($event_name);
$this->preDispatch($event_name, $event);
$e = $this->stopwatch->start($event_name, 'section');
if (isset($this->listeners[$event_name])) {
// Sort listeners if necessary.
if (isset($this->unsorted[$event_name])) {
krsort($this->listeners[$event_name]);
unset($this->unsorted[$event_name]);
}
// Invoke listeners and resolve callables if necessary.
foreach ($this->listeners[$event_name] as $priority => &$definitions) {
foreach ($definitions as $key => &$definition) {
if (!isset($definition['callable'])) {
$definition['callable'] = [
$this->container->get($definition['service'][0]),
$definition['service'][1],
];
}
$definition['callable']($event, $event_name, $this);
$this->addCalledListener($definition, $event_name, $priority);
if ($event->isPropagationStopped()) {
return $event;
}
}
}
}
if ($e->isStarted()) {
$e->stop();
}
$this->postDispatch($event_name, $event);
return $event;
}
/**
* {@inheritdoc}
*/
public function getCalledListeners() {
return $this->calledListeners;
}
/**
* {@inheritdoc}
*/
public function getNotCalledListeners() {
return $this->notCalledListeners;
}
/**
* Called before dispatching the event.
*
* @param string $eventName The event name
* @param Event $event The event
*/
protected function preDispatch($eventName, Event $event) {
switch ($eventName) {
case KernelEvents::VIEW:
case KernelEvents::RESPONSE:
// stop only if a controller has been executed
if ($this->stopwatch->isStarted('controller')) {
$this->stopwatch->stop('controller');
}
break;
}
}
/**
* Called after dispatching the event.
*
* @param string $eventName The event name
* @param Event $event The event
*/
protected function postDispatch($eventName, Event $event) {
switch ($eventName) {
case KernelEvents::CONTROLLER:
$this->stopwatch->start('controller', 'section');
break;
case KernelEvents::RESPONSE:
$token = $event->getResponse()->headers->get('X-Debug-Token');
try {
$this->stopwatch->stopSection($token);
}
catch (\LogicException $e) {
}
break;
case KernelEvents::TERMINATE:
// In the special case described in the `preDispatch` method above, the `$token` section
// does not exist, then closing it throws an exception which must be caught.
$token = $event->getResponse()->headers->get('X-Debug-Token');
try {
$this->stopwatch->stopSection($token);
}
catch (\LogicException $e) {
}
break;
}
}
/**
* @param $definition
* @param $event_name
* @param $priority
*/
private function addCalledListener($definition, $event_name, $priority) {
$this->calledListeners[$event_name][$priority][] = [
'class' => get_class($definition['callable'][0]),
'method' => $definition['callable'][1],
];
// Remove this listener from the $notCalledListeners array.
if (!$this->notCalledListeners) {
$this->notCalledListeners = $this->clone($this->listeners);
}
foreach ($this->notCalledListeners[$event_name][$priority] as $key => $listener) {
if ($listener['service'][0] == $definition['service'][0] && $listener['service'][1] == $definition['service'][1]) {
unset($this->notCalledListeners[$event_name][$priority][$key]);
}
}
}
/**
* @param $listeners
*
* @return array
*/
private function clone($listeners) {
$clone = [];
foreach ($listeners as $eventName => $events) {
foreach ($events as $priorityValue => $priorities) {
foreach ($priorities as $key => $listener) {
$clone[$eventName][$priorityValue][$key]['service'] = [
$listener['service'][0],
$listener['service'][1]
];
}
}
}
return $clone;
}
}
<?php
namespace Drupal\webprofiler;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher as BaseTraceableEventDispatcher;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\Event;
/**
* Class TraceableEventDispatcher
*/
class TraceableEventDispatcher extends BaseTraceableEventDispatcher {
/**
* {@inheritdoc}
*/
protected function preDispatch($eventName, Event $event) {
switch ($eventName) {
case KernelEvents::VIEW:
case KernelEvents::RESPONSE:
// stop only if a controller has been executed
if ($this->stopwatch->isStarted('controller')) {
$this->stopwatch->stop('controller');
}
break;
}
}
/**
* {@inheritdoc}
*/
protected function postDispatch($eventName, Event $event) {
switch ($eventName) {
case KernelEvents::CONTROLLER:
$this->stopwatch->start('controller', 'section');
break;
case KernelEvents::RESPONSE:
$token = $event->getResponse()->headers->get('X-Debug-Token');
try {
$this->stopwatch->stopSection($token);
} catch (\LogicException $e) {
}
break;
case KernelEvents::TERMINATE:
// In the special case described in the `preDispatch` method above, the `$token` section
// does not exist, then closing it throws an exception which must be caught.
$token = $event->getResponse()->headers->get('X-Debug-Token');
try {
$this->stopwatch->stopSection($token);
} catch (\LogicException $e) {
}
break;
}
}
/**
* {@inheritdoc}
*/
public function getListenerPriority($eventName, $listener) {
if (!isset($this->listeners[$eventName])) {
return;
}
foreach ($this->listeners[$eventName] as $priority => $listeners) {
if (FALSE !== ($key = array_search($listener, $listeners, TRUE))) {
return $priority;
}
}
}
}
......@@ -28,7 +28,6 @@ class WebprofilerServiceProvider extends ServiceProviderBase {
$container->addCompilerPass(new StoragePass());
$container->addCompilerPass(new ServicePass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new EventPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new DecoratorPass(), PassConfig::TYPE_AFTER_REMOVING);
$modules = $container->getParameter('container.modules');
......@@ -57,7 +56,7 @@ class WebprofilerServiceProvider extends ServiceProviderBase {
'priority' => 78,
]);
}
// Add TranslationsDataCollector only if Locale module is enabled.
if (isset($modules['locale'])) {
$container->register('webprofiler.translations', 'Drupal\webprofiler\DataCollector\TranslationsDataCollector')
......@@ -105,5 +104,14 @@ class WebprofilerServiceProvider extends ServiceProviderBase {
// Replace the regular string_translation service with a traceable one.
$container->getDefinition('string_translation')
->setClass('Drupal\webprofiler\StringTranslation\TranslationManagerWrapper');
// Replace the regular event_dispatcher service with a traceable one.
$container->getDefinition('event_dispatcher')
->setClass('Drupal\webprofiler\EventDispatcher\TraceableEventDispatcher')
->addMethodCall('setStopwatch', [new Reference('stopwatch')]);
$container->getDefinition('http_kernel.basic')
->replaceArgument(1, new Reference('webprofiler.debug.controller_resolver'));
}
}
{% block toolbar %}
{% set icon %}
<a href="{{ url("webprofiler.dashboard", {profile: token}, {fragment: 'events'}) }}" title="{{ 'Events'|t }}">
<img width="20" height="28" alt="{{ 'Events'|t }}"
src="data:image/png;base64,{{ collector.icon }}">
<span class="sf-toolbar-info-piece-additional sf-toolbar-status">{{ collector.getCalledListenersCount }}</span>
</a>
<a href="{{ url("webprofiler.dashboard", {profile: token}, {fragment: 'events'}) }}" title="{{ 'Events'|t }}">
<img width="20" height="28" alt="{{ 'Events'|t }}"
src="data:image/png;base64,{{ collector.icon }}">
<span class="sf-toolbar-info-piece-additional sf-toolbar-status">{{ collector.getCalledListenersCount }}</span>
</a>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>{{ 'Triggered'|t }}</b>
<span>{{ collector.getCalledListenersCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>{{ 'Not triggered'|t }}</b>
<span>{{ collector.getNotCalledListenersCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>{{ 'Called'|t }}</b>
<span>{{ collector.getCalledListenersCount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>{{ 'Not called'|t }}</b>
<span>{{ collector.getNotCalledListenersCount }}</span>
</div>
{% endset %}
<div class="sf-toolbar-block">
......@@ -36,44 +37,46 @@
<th>{{ 'Class'|t }}</th>
<th>{{ 'Priority'|t }}</th>
</thead>
<tbody> <% _.each( data.called_listeners, function( item ){ %>
<tr>
<td><%= item.event %></td>
<% if(item.type == 'Method') { %>
<td>
<%= Drupal.webprofiler.helpers.classLink(item) %>
</td>
<% } else { %>
<td>{{ 'Closure'|t }}</td>
<% } %>
<td><%= item.priority %></td>
</tr>
<tbody>
<% _.each( data.called_listeners, function( events, event_name ){ %>
<% _.each( events, function( priority, priority_value ){ %>
<% _.each( priority, function( listener ){ %>
<tr>
<td><%= event_name %></td>
<% if( listener.clazz ) { %>
<td><%= Drupal.webprofiler.helpers.classLink(listener.clazz) %></td>
<% } else { %>
<td><%= listener.service[0] %>::<%= listener.service[1] %></td>
<% } %>
<td><%= priority_value %></td>
</tr>
<% }); %>
<% }); %>
<% }); %>
</tbody>
</table>
</div>
<div class="panel__container">
<table class="table--duo">
<thead>
<th>{{ 'Non called listeners'|t }}</th>
<th>{{ 'Class'|t }}</th>
<th>{{ 'Not called listeners'|t }}</th>
<th>{{ 'Service'|t }}</th>
<th>{{ 'Priority'|t }}</th>
</thead>
<tbody>
<% _.each( data.not_called_listeners, function( item ){ %>
<% _.each( data.not_called_listeners, function( events, event_name ){ %>
<% _.each( events, function( priority, priority_value ){ %>
<% _.each( priority, function( listener ){ %>
<tr>
<td><%= item.event %></td>
<% if(item.type == 'Method') { %>
<td>
<%= Drupal.webprofiler.helpers.classLink(item) %>
</td>
<% } else { %>
<td>{{ 'Closure'|t }}</td>
<% } %>
<td><%= event_name %></td>
<td><%= listener.service[0] %>::<%= listener.service[1] %></td>
<td><%= priority_value %></td>
</tr>
<% }); %>
<% }); %>
<% }); %>
</tbody>
</table>
</div>
</script>
{% endblock %}
......@@ -187,10 +187,6 @@ services:
stopwatch:
class: Drupal\webprofiler\Stopwatch
webprofiler.debug.event_dispatcher.default:
class: Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher
arguments: ['@service_container']
webprofiler.debug.plugin.manager.mail.default:
class: Drupal\Core\Mail\MailManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@config.factory', '@logger.factory', '@string_translation', '@renderer']
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment