feature#9170 Decoupled TraceableEventDispatcher from the Profiler (fabpot)

This PR was merged into the master branch.

Discussion
----------

Decoupled TraceableEventDispatcher from the Profiler

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

This PR removes the Profiler dependency on the TraceableEventDispatcher. That makes things more decoupled and cleaner. This PR also cleans up how profiles are stored; a Profile is now always stored only once.

I've created a `LateDataCollectorInterface` that is implemented for data collector that needs to get information from data that are available very late in the request process (when the request and the response are not even available anymore). The `lateCollect()` method is called just before the profile is stored.

We have 3 data collectors that implement that interface:

 * Time: As the traceable event dipsatcher gets inject timing information via the stopwatch about all events (including the `terminate` one), we need to get events from the stopwatch as late as possible.
 * Event: The traceable event dispatcher gathers all called listeners to determine non-called ones. To be able to accurately do that for all events (including the `terminate` one), we need to get the data as late as possible.
 * Memory: We want to get the memory as late as possible to get the most accurate number as possible

I'm not very happy with the name and as always, better suggestions would be much appreciated.

This is an extract from #9168

Commits
-------

5cedea2 [HttpKernel] added LateDataCollectorInterface
9c4bc9a [HttpKernel] decoupled TraceableEventDispatcher and Profiler
This commit is contained in:
Fabien Potencier 2013-09-30 17:42:20 +02:00
commit bdcdc6eddb
11 changed files with 145 additions and 119 deletions

View File

@ -34,6 +34,7 @@
<service id="data_collector.events" class="%data_collector.events.class%" public="false">
<tag name="data_collector" template="@WebProfiler/Collector/events.html.twig" id="events" priority="255" />
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
</service>
<service id="data_collector.logger" class="%data_collector.logger.class%" public="false">
@ -45,6 +46,7 @@
<service id="data_collector.time" class="%data_collector.time.class%" public="false">
<tag name="data_collector" template="@WebProfiler/Collector/time.html.twig" id="time" priority="255" />
<argument type="service" id="kernel" on-invalid="ignore" />
<argument type="service" id="debug.stopwatch" on-invalid="ignore" />
</service>
<service id="data_collector.memory" class="%data_collector.memory.class%" public="false">

View File

@ -19,7 +19,6 @@
<argument type="service" id="event_dispatcher" />
<argument type="service" id="debug.stopwatch" />
<argument type="service" id="logger" on-invalid="null" />
<call method="setProfiler"><argument type="service" id="profiler" on-invalid="null" /></call>
</service>
<service id="debug.controller_resolver" class="%debug.controller_resolver.class%">

View File

@ -29,6 +29,7 @@
<argument type="service" id="profiler.request_matcher" on-invalid="null" />
<argument>%profiler_listener.only_exceptions%</argument>
<argument>%profiler_listener.only_master_requests%</argument>
<argument type="service" id="request_stack" />
</service>
</services>
</container>

View File

@ -11,8 +11,10 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface;
/**
@ -20,8 +22,15 @@ use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class EventDataCollector extends DataCollector
class EventDataCollector extends DataCollector implements LateDataCollectorInterface
{
protected $dispatcher;
public function __construct(EventDispatcherInterface $dispatcher = null)
{
$this->dispatcher = $dispatcher;
}
/**
* {@inheritdoc}
*/
@ -33,6 +42,14 @@ class EventDataCollector extends DataCollector
);
}
public function lateCollect()
{
if ($this->dispatcher instanceof TraceableEventDispatcherInterface) {
$this->setCalledListeners($this->dispatcher->getCalledListeners());
$this->setNotCalledListeners($this->dispatcher->getNotCalledListeners());
}
}
/**
* Sets the called listeners.
*

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* LateDataCollectorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface LateDataCollectorInterface
{
/**
* Collects data as late as possible.
*/
public function lateCollect();
}

View File

@ -32,6 +32,14 @@ class LoggerDataCollector extends DataCollector
}
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
// everything is done as late as possible
}
/**
* {@inheritdoc}
*/

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@ -19,7 +20,7 @@ use Symfony\Component\HttpFoundation\Response;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MemoryDataCollector extends DataCollector
class MemoryDataCollector extends DataCollector implements LateDataCollectorInterface
{
public function __construct()
{
@ -37,6 +38,14 @@ class MemoryDataCollector extends DataCollector
$this->updateMemoryUsage();
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$this->updateMemoryUsage();
}
/**
* Gets the memory.
*

View File

@ -12,22 +12,26 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* TimeDataCollector.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TimeDataCollector extends DataCollector
class TimeDataCollector extends DataCollector implements LateDataCollectorInterface
{
protected $kernel;
protected $stopwatch;
public function __construct(KernelInterface $kernel = null)
public function __construct(KernelInterface $kernel = null, $stopwatch = null)
{
$this->kernel = $kernel;
$this->stopwatch = $stopwatch;
}
/**
@ -42,11 +46,23 @@ class TimeDataCollector extends DataCollector
}
$this->data = array(
'token' => $response->headers->get('X-Debug-Token'),
'start_time' => $startTime * 1000,
'events' => array(),
);
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
if (null !== $this->stopwatch && isset($this->data['token'])) {
$this->setEvents($this->stopwatch->getSectionEvents($this->data['token']));
}
unset($this->data['token']);
}
/**
* Sets the request events.
*

View File

@ -14,8 +14,6 @@ namespace Symfony\Component\HttpKernel\Debug;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\HttpKernel\KernelEvents;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -33,7 +31,6 @@ class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEve
private $logger;
private $called;
private $stopwatch;
private $profiler;
private $dispatcher;
private $wrappedListeners;
private $firstCalledEvent;
@ -59,11 +56,16 @@ class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEve
/**
* Sets the profiler.
*
* The traceable event dispatcher does not use the profiler anymore.
* The job is now done directly by the Profiler listener and the
* data collectors themselves.
*
* @param Profiler|null $profiler A Profiler instance
*
* @deprecated Deprecated since version 2.4, to be removed in 3.0.
*/
public function setProfiler(Profiler $profiler = null)
{
$this->profiler = $profiler;
}
/**
@ -142,7 +144,9 @@ class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEve
unset($this->firstCalledEvent[$eventName]);
$e->stop();
if ($e->isStarted()) {
$e->stop();
}
$this->postDispatch($eventName, $event);
@ -312,57 +316,6 @@ class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEve
return $info;
}
/**
* Updates the stopwatch data in the profile hierarchy.
*
* @param string $token Profile token
* @param Boolean $updateChildren Whether to update the children altogether
*/
private function updateProfiles($token, $updateChildren)
{
if (!$this->profiler || !$profile = $this->profiler->loadProfile($token)) {
return;
}
$this->saveInfoInProfile($profile, $updateChildren);
}
/**
* Update the profiles with the timing and events information and saves them.
*
* @param Profile $profile The root profile
* @param Boolean $updateChildren Whether to update the children altogether
*/
private function saveInfoInProfile(Profile $profile, $updateChildren)
{
try {
$collector = $profile->getCollector('memory');
$collector->updateMemoryUsage();
} catch (\InvalidArgumentException $e) {
}
try {
$collector = $profile->getCollector('time');
$collector->setEvents($this->stopwatch->getSectionEvents($profile->getToken()));
} catch (\InvalidArgumentException $e) {
}
try {
$collector = $profile->getCollector('events');
$collector->setCalledListeners($this->getCalledListeners());
$collector->setNotCalledListeners($this->getNotCalledListeners());
} catch (\InvalidArgumentException $e) {
}
$this->profiler->saveProfile($profile);
if ($updateChildren) {
foreach ($profile->getChildren() as $child) {
$this->saveInfoInProfile($child, true);
}
}
}
private function preDispatch($eventName, Event $event)
{
// wrap all listeners before they are called
@ -411,23 +364,14 @@ class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEve
case KernelEvents::RESPONSE:
$token = $event->getResponse()->headers->get('X-Debug-Token');
$this->stopwatch->stopSection($token);
if ($event->isMasterRequest()) {
// The profiles can only be updated once they have been created
// that is after the 'kernel.response' event of the main request
$this->updateProfiles($token, true);
}
break;
case KernelEvents::TERMINATE:
$token = $event->getResponse()->headers->get('X-Debug-Token');
// 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) {}
// The children profiles have been updated by the previous 'kernel.response'
// event. Only the root profile need to be updated with the 'kernel.terminate'
// timing informations.
$this->updateProfiles($token, false);
break;
}
@ -448,7 +392,9 @@ class TraceableEventDispatcher implements EventDispatcherInterface, TraceableEve
call_user_func($listener, $event, $eventName, $self);
$e->stop();
if ($e->isStarted()) {
$e->stop();
}
if ($event->isPropagationStopped()) {
$self->logSkippedListeners($eventName, $event, $listener);

View File

@ -11,13 +11,16 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Profiler\Profile;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
@ -32,9 +35,10 @@ class ProfilerListener implements EventSubscriberInterface
protected $onlyException;
protected $onlyMasterRequests;
protected $exception;
protected $children;
protected $requests;
protected $profiles;
protected $requestStack;
protected $parents;
/**
* Constructor.
@ -44,14 +48,16 @@ class ProfilerListener implements EventSubscriberInterface
* @param Boolean $onlyException true if the profiler only collects data when an exception occurs, false otherwise
* @param Boolean $onlyMasterRequests true if the profiler only collects data when the request is a master request, false otherwise
*/
public function __construct(Profiler $profiler, RequestMatcherInterface $matcher = null, $onlyException = false, $onlyMasterRequests = false)
public function __construct(Profiler $profiler, RequestMatcherInterface $matcher = null, $onlyException = false, $onlyMasterRequests = false, RequestStack $requestStack = null)
{
$this->profiler = $profiler;
$this->matcher = $matcher;
$this->onlyException = (Boolean) $onlyException;
$this->onlyMasterRequests = (Boolean) $onlyMasterRequests;
$this->children = new \SplObjectStorage();
$this->profiles = array();
$this->profiles = new \SplObjectStorage();
$this->parents = new \SplObjectStorage();
$this->requests = array();
$this->requestStack = $requestStack;
}
/**
@ -68,9 +74,14 @@ class ProfilerListener implements EventSubscriberInterface
$this->exception = $event->getException();
}
/**
* @deprecated Deprecated since version 2.4, to be removed in 3.0.
*/
public function onKernelRequest(GetResponseEvent $event)
{
$this->requests[] = $event->getRequest();
if (null === $this->requestStack) {
$this->requests[] = $event->getRequest();
}
}
/**
@ -101,42 +112,35 @@ class ProfilerListener implements EventSubscriberInterface
return;
}
$this->profiles[] = $profile;
$this->profiles[$request] = $profile;
if (null !== $exception) {
foreach ($this->profiles as $profile) {
$this->profiler->saveProfile($profile);
}
return;
}
// keep the profile as the child of its parent
if (!$master) {
if (null !== $this->requestStack) {
$this->parents[$request] = $this->requestStack->getParentRequest();
} elseif (!$master) {
// to be removed when requestStack is required
array_pop($this->requests);
$parent = end($this->requests);
$this->parents[$request] = end($this->requests);
}
}
// when simulating requests, we might not have the parent
if ($parent) {
$profiles = isset($this->children[$parent]) ? $this->children[$parent] : array();
$profiles[] = $profile;
$this->children[$parent] = $profiles;
public function onKernelTerminate(PostResponseEvent $event)
{
// attach children to parents
foreach ($this->profiles as $request) {
if ($parentRequest = $this->parents[$request]) {
$this->profiles[$parentRequest]->addChild($this->profiles[$request]);
}
}
if (isset($this->children[$request])) {
foreach ($this->children[$request] as $child) {
$profile->addChild($child);
}
$this->children[$request] = array();
// save profiles
foreach ($this->profiles as $request) {
$this->profiler->saveProfile($this->profiles[$request]);
}
if ($master) {
$this->saveProfiles($profile);
$this->children = new \SplObjectStorage();
}
$this->profiles = new \SplObjectStorage();
$this->parents = new \SplObjectStorage();
$this->requests = array();
}
public static function getSubscribedEvents()
@ -147,19 +151,7 @@ class ProfilerListener implements EventSubscriberInterface
KernelEvents::REQUEST => array('onKernelRequest', 1024),
KernelEvents::RESPONSE => array('onKernelResponse', -100),
KernelEvents::EXCEPTION => 'onKernelException',
KernelEvents::TERMINATE => array('onKernelTerminate', -1024),
);
}
/**
* Saves the profile hierarchy.
*
* @param Profile $profile The root profile
*/
private function saveProfiles(Profile $profile)
{
$this->profiler->saveProfile($profile);
foreach ($profile->getChildren() as $profile) {
$this->saveProfiles($profile);
}
}
}

View File

@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Profiler\ProfilerStorageInterface;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Psr\Log\LoggerInterface;
/**
@ -109,6 +110,13 @@ class Profiler
*/
public function saveProfile(Profile $profile)
{
// late collect
foreach ($profile->getCollectors() as $collector) {
if ($collector instanceof LateDataCollectorInterface) {
$collector->lateCollect();
}
}
if (!($ret = $this->storage->write($profile)) && null !== $this->logger) {
$this->logger->warning('Unable to store the profiler information.');
}
@ -215,8 +223,8 @@ class Profiler
foreach ($this->collectors as $collector) {
$collector->collect($request, $response, $exception);
// forces collectors to become "read/only" (they loose their object dependencies)
$profile->addCollector(unserialize(serialize($collector)));
// we need to clone for sub-requests
$profile->addCollector(clone $collector);
}
return $profile;