added Stopwatch support in debug mode, added a timeline representing the stopwatch events in the web profiler
Enjoy!
This commit is contained in:
parent
106ebdbe18
commit
842ac36f33
|
@ -12,6 +12,7 @@
|
|||
namespace Symfony\Bridge\Doctrine\Logger;
|
||||
|
||||
use Symfony\Component\HttpKernel\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\Debug\Stopwatch;
|
||||
use Doctrine\DBAL\Logging\DebugStack;
|
||||
|
||||
/**
|
||||
|
@ -22,15 +23,18 @@ use Doctrine\DBAL\Logging\DebugStack;
|
|||
class DbalLogger extends DebugStack
|
||||
{
|
||||
protected $logger;
|
||||
protected $stopwatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param LoggerInterface $logger A LoggerInterface instance
|
||||
* @param LoggerInterface $logger A LoggerInterface instance
|
||||
* @param Stopwatch $stopwatch A Stopwatch instance
|
||||
*/
|
||||
public function __construct(LoggerInterface $logger = null)
|
||||
public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch = null)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->stopwatch = $stopwatch;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,11 +44,27 @@ class DbalLogger extends DebugStack
|
|||
{
|
||||
parent::startQuery($sql, $params, $types);
|
||||
|
||||
if (null !== $this->stopwatch) {
|
||||
$this->stopwatch->start('doctrine', 'doctrine');
|
||||
}
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->log($sql.' ('.json_encode($params).')');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stopQuery()
|
||||
{
|
||||
parent::stopQuery();
|
||||
|
||||
if (null !== $this->stopwatch) {
|
||||
$this->stopwatch->stop('doctrine');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message.
|
||||
*
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
<service id="doctrine.dbal.logger" class="%doctrine.dbal.logger.class%" public="false">
|
||||
<tag name="monolog.logger" channel="doctrine" />
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
<argument type="service" id="debug.stopwatch" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="data_collector.doctrine" class="%doctrine.data_collector.class%" public="false">
|
||||
|
|
|
@ -140,6 +140,11 @@ class ContainerAwareEventDispatcher extends EventDispatcher
|
|||
parent::dispatch($eventName, $event);
|
||||
}
|
||||
|
||||
public function getContainer()
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily loads listeners for this event from the dependency injection
|
||||
* container.
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<?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\Bundle\FrameworkBundle\Controller;
|
||||
|
||||
use Symfony\Component\HttpKernel\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\Debug\Stopwatch;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
|
||||
|
||||
/**
|
||||
* TraceableControllerResolver.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TraceableControllerResolver extends ControllerResolver
|
||||
{
|
||||
private $stopwatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param ContainerInterface $container A ContainerInterface instance
|
||||
* @param ControllerNameParser $parser A ControllerNameParser instance
|
||||
* @param Stopwatch $stopwatch A Stopwatch instance
|
||||
* @param LoggerInterface $logger A LoggerInterface instance
|
||||
*/
|
||||
public function __construct(ContainerInterface $container, ControllerNameParser $parser, Stopwatch $stopwatch, LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($container, $parser, $logger);
|
||||
|
||||
$this->stopwatch = $stopwatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @{inheritdoc}
|
||||
*/
|
||||
public function getController(Request $request)
|
||||
{
|
||||
$e = $this->stopwatch->start('controller.get_callable');
|
||||
|
||||
$ret = parent::getController($request);
|
||||
|
||||
$e->stop();
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @{inheritdoc}
|
||||
*/
|
||||
public function getArguments(Request $request, $controller)
|
||||
{
|
||||
$e = $this->stopwatch->start('controller.get_arguments');
|
||||
|
||||
$ret = parent::getArguments($request, $controller);
|
||||
|
||||
$e->stop();
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -12,10 +12,13 @@
|
|||
namespace Symfony\Bundle\FrameworkBundle\Debug;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\ContainerAwareEventDispatcher;
|
||||
use Symfony\Component\HttpKernel\Debug\Stopwatch;
|
||||
use Symfony\Component\HttpKernel\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Symfony\Component\HttpKernel\Profiler\Profiler;
|
||||
use Symfony\Component\HttpKernel\Profiler\Profile;
|
||||
|
||||
/**
|
||||
* Extends the ContainerAwareEventDispatcher to add some debugging tools.
|
||||
|
@ -26,21 +29,53 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
|
|||
{
|
||||
private $logger;
|
||||
private $called;
|
||||
private $stopwatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param ContainerInterface $container A ContainerInterface instance
|
||||
* @param Stopwatch $stopwatch A Stopwatch instance
|
||||
* @param LoggerInterface $logger A LoggerInterface instance
|
||||
*/
|
||||
public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
|
||||
public function __construct(ContainerInterface $container, Stopwatch $stopwatch, LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($container);
|
||||
|
||||
$this->stopwatch = $stopwatch;
|
||||
$this->logger = $logger;
|
||||
$this->called = array();
|
||||
}
|
||||
|
||||
public function dispatch($eventName, Event $event = null)
|
||||
{
|
||||
if ('kernel.request' === $eventName) {
|
||||
$this->stopwatch->startSection();
|
||||
} elseif ('kernel.view' === $eventName || 'kernel.response' === $eventName) {
|
||||
// stop only if a controller has been executed
|
||||
try {
|
||||
$this->stopwatch->stop('controller');
|
||||
} catch (\LogicException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$e1 = $this->stopwatch->start($eventName, 'section');
|
||||
|
||||
parent::dispatch($eventName, $event);
|
||||
|
||||
$e1->stop();
|
||||
|
||||
if ('kernel.controller' === $eventName) {
|
||||
$this->stopwatch->start('controller', 'section');
|
||||
} elseif ('kernel.response' === $eventName) {
|
||||
$token = $event->getResponse()->headers->get('X-Debug-Token');
|
||||
|
||||
$this->stopwatch->stopSection($token);
|
||||
|
||||
$this->updateProfile($token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
|
@ -79,8 +114,12 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
|
|||
|
||||
$this->called[$eventName.'.'.$info['pretty']] = $info;
|
||||
|
||||
$e2 = $this->stopwatch->start(substr($info['class'], strrpos($info['class'], '\\') + 1), 'event_listener');
|
||||
|
||||
call_user_func($listener, $event);
|
||||
|
||||
$e2->stop();
|
||||
|
||||
if ($event->isPropagationStopped()) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName));
|
||||
|
@ -115,6 +154,18 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function lazyLoad($eventName)
|
||||
{
|
||||
$e = $this->stopwatch->start($eventName.'.loading', 'event_listener_loading');
|
||||
|
||||
parent::lazyLoad($eventName);
|
||||
|
||||
$e->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
@ -199,4 +250,25 @@ class TraceableEventDispatcher extends ContainerAwareEventDispatcher implements
|
|||
|
||||
return $info;
|
||||
}
|
||||
|
||||
private function updateProfile($token)
|
||||
{
|
||||
if (!$this->getContainer()->has('profiler')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$profiler = $this->getContainer()->get('profiler');
|
||||
if (!$profile = $profiler->loadProfile($token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$profile->getCollector('time')->setEvents($this->stopwatch->getSectionEvents($profile->getToken()));
|
||||
$profiler->saveProfile($profile);
|
||||
|
||||
// children
|
||||
foreach ($profile->getChildren() as $child) {
|
||||
$child->getCollector('time')->setEvents($this->stopwatch->getSectionEvents($child->getToken()));
|
||||
$profiler->saveProfile($child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,9 @@ class FrameworkExtension extends Extension
|
|||
$loader->load('debug.xml');
|
||||
$container->setDefinition('event_dispatcher', $container->findDefinition('debug.event_dispatcher'));
|
||||
$container->setAlias('debug.event_dispatcher', 'event_dispatcher');
|
||||
|
||||
$container->setDefinition('controller_resolver', $container->findDefinition('debug.controller_resolver'));
|
||||
$container->setAlias('debug.controller_resolver', 'controller_resolver');
|
||||
}
|
||||
|
||||
$configuration = new Configuration($container->getParameter('kernel.debug'));
|
||||
|
|
|
@ -21,6 +21,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|||
/**
|
||||
* This HttpKernel is used to manage scope changes of the DI container.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class HttpKernel extends BaseHttpKernel
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<parameter key="data_collector.exception.class">Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector</parameter>
|
||||
<parameter key="data_collector.events.class">Symfony\Component\HttpKernel\DataCollector\EventDataCollector</parameter>
|
||||
<parameter key="data_collector.logger.class">Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector</parameter>
|
||||
<parameter key="data_collector.timer.class">Symfony\Component\HttpKernel\DataCollector\TimerDataCollector</parameter>
|
||||
<parameter key="data_collector.time.class">Symfony\Component\HttpKernel\DataCollector\TimeDataCollector</parameter>
|
||||
<parameter key="data_collector.memory.class">Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector</parameter>
|
||||
</parameters>
|
||||
|
||||
|
@ -21,7 +21,6 @@
|
|||
</service>
|
||||
|
||||
<service id="data_collector.request" class="%data_collector.request.class%">
|
||||
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
|
||||
<tag name="data_collector" template="WebProfilerBundle:Collector:request" id="request" priority="255" />
|
||||
</service>
|
||||
|
||||
|
@ -42,8 +41,8 @@
|
|||
<argument type="service" id="logger" on-invalid="ignore" />
|
||||
</service>
|
||||
|
||||
<service id="data_collector.timer" class="%data_collector.timer.class%" public="false">
|
||||
<tag name="data_collector" template="WebProfilerBundle:Collector:timer" id="timer" priority="255" />
|
||||
<service id="data_collector.time" class="%data_collector.time.class%" public="false">
|
||||
<tag name="data_collector" template="WebProfilerBundle:Collector:time" id="time" priority="255" />
|
||||
<argument type="service" id="kernel" />
|
||||
</service>
|
||||
|
||||
|
|
|
@ -6,14 +6,27 @@
|
|||
|
||||
<parameters>
|
||||
<parameter key="debug.event_dispatcher.class">Symfony\Bundle\FrameworkBundle\Debug\TraceableEventDispatcher</parameter>
|
||||
<parameter key="debug.stopwatch.class">Symfony\Component\HttpKernel\Debug\Stopwatch</parameter>
|
||||
<parameter key="debug.container.dump">%kernel.cache_dir%/%kernel.container_class%.xml</parameter>
|
||||
<parameter key="debug.controller_resolver.class">Symfony\Bundle\FrameworkBundle\Controller\TraceableControllerResolver</parameter>
|
||||
</parameters>
|
||||
|
||||
<services>
|
||||
<service id="debug.stopwatch" class="%debug.stopwatch.class%" />
|
||||
|
||||
<service id="debug.event_dispatcher" class="%debug.event_dispatcher.class%">
|
||||
<tag name="monolog.logger" channel="event" />
|
||||
<argument type="service" id="service_container" />
|
||||
<argument type="service" id="debug.stopwatch" />
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="debug.controller_resolver" class="%debug.controller_resolver.class%">
|
||||
<tag name="monolog.logger" channel="request" />
|
||||
<argument type="service" id="service_container" />
|
||||
<argument type="service" id="controller_name_converter" />
|
||||
<argument type="service" id="debug.stopwatch" />
|
||||
<argument type="service" id="logger" on-invalid="ignore" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Debug;
|
|||
|
||||
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Debug\TraceableEventDispatcher;
|
||||
use Symfony\Component\HttpKernel\Debug\Stopwatch;
|
||||
|
||||
class TraceableEventDispatcherTest extends TestCase
|
||||
{
|
||||
|
@ -23,7 +24,7 @@ class TraceableEventDispatcherTest extends TestCase
|
|||
public function testThrowsAnExceptionWhenAListenerMethodIsNotCallable()
|
||||
{
|
||||
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
|
||||
$dispatcher = new TraceableEventDispatcher($container);
|
||||
$dispatcher = new TraceableEventDispatcher($container, new Stopwatch());
|
||||
$dispatcher->addListener('onFooEvent', new \stdClass());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?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\Bundle\TwigBundle\Debug;
|
||||
|
||||
use Symfony\Bundle\TwigBundle\TwigEngine;
|
||||
use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables;
|
||||
use Symfony\Component\Templating\TemplateNameParserInterface;
|
||||
use Symfony\Component\HttpKernel\Debug\Stopwatch;
|
||||
|
||||
/**
|
||||
* Times the time spent to render a template.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TimedTwigEngine extends TwigEngine
|
||||
{
|
||||
protected $stopwatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Twig_Environment $environment A \Twig_Environment instance
|
||||
* @param TemplateNameParserInterface $parser A TemplateNameParserInterface instance
|
||||
* @param GlobalVariables|null $globals A GlobalVariables instance or null
|
||||
* @param Stopwatch $stopwatch A Stopwatch instance
|
||||
*/
|
||||
public function __construct(\Twig_Environment $environment, TemplateNameParserInterface $parser, Stopwatch $stopwatch, GlobalVariables $globals = null)
|
||||
{
|
||||
parent::__construct($environment, $parser, $globals);
|
||||
|
||||
$this->stopwatch = $stopwatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render($name, array $parameters = array())
|
||||
{
|
||||
$e = $this->stopwatch->start(sprintf('template.twig (%s)', $name), 'template');
|
||||
|
||||
$ret = $this->load($name)->render($parameters);
|
||||
|
||||
$e->stop();
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -63,6 +63,13 @@ class TwigExtension extends Extension
|
|||
|
||||
$container->setParameter('twig.options', $config);
|
||||
|
||||
if ($container->getParameter('kernel.debug')) {
|
||||
$loader->load('debug.xml');
|
||||
|
||||
$container->setDefinition('templating.engine.twig', $container->findDefinition('debug.templating.engine.twig'));
|
||||
$container->setAlias('debug.templating.engine.twig', 'templating.engine.twig');
|
||||
}
|
||||
|
||||
$this->addClassesToCompile(array(
|
||||
'Twig_Environment',
|
||||
'Twig_ExtensionInterface',
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" ?>
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<parameters>
|
||||
<parameter key="debug.templating.engine.twig.class">Symfony\Bundle\TwigBundle\Debug\TimedTwigEngine</parameter>
|
||||
</parameters>
|
||||
|
||||
<services>
|
||||
<service id="debug.templating.engine.twig" class="%debug.templating.engine.twig.class%" public="false">
|
||||
<argument type="service" id="twig" />
|
||||
<argument type="service" id="templating.name_parser" />
|
||||
<argument type="service" id="debug.stopwatch" />
|
||||
<argument type="service" id="templating.globals" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
|
@ -15,7 +15,6 @@ use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
|
|||
use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables;
|
||||
use Symfony\Component\Templating\TemplateNameParserInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* This engine knows how to render Twig templates.
|
||||
|
|
|
@ -427,3 +427,9 @@ td.main, td.menu {
|
|||
#navigation .import input {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
background-color: #fbfbfb;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
|
@ -0,0 +1,323 @@
|
|||
{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}
|
||||
|
||||
{% if colors is not defined %}
|
||||
{% set colors = {
|
||||
'default': '#aacd4e',
|
||||
'section': '#666',
|
||||
'event_listener': '#3dd',
|
||||
'event_listener_loading': '#add',
|
||||
'template': '#dd3',
|
||||
'doctrine': '#d3d',
|
||||
'child_sections': '#eed',
|
||||
} %}
|
||||
{% endif %}
|
||||
|
||||
{% block toolbar %}
|
||||
{% set icon %}
|
||||
<img width="16" height="28" alt="Time" style="vertical-align: middle; margin-right: 5px;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAcCAYAAABoMT8aAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAiNJREFUeNpi/P//PwMlgImBQjDwBrCcO3cOq0RRUdF3ZH5fXx8nTVzAePbsWcq8gMwxMjJiSUlJcXv9+nXm169fbf78+SMAVsTC8paXl3ePmJjYjJkzZx4GevsviheAGhmBguL+/v4779y5s/Xjx48+MM0gAGQLv3//PvzmzZv7AwMD19y+fVsEpAfsBWBCYly8eLHcsmXLjnz//l2GGGcDXXM1IyPD2dvb+xXIBTwbN25chU3zgQMHwBgdfP78WXvp0qVzgUwuprq6utg3b96YkRp4z549854wYYI7071791LJjYFLly7lM7148UKHXAOALtdnAYYwCyGFyOHg4OAAZ3/69ImfopTIzMz8j4WVlfXf79+/sRqEbBs2wMfH94tJXV39DbkuUFFReclkb29/jlwDPD09jzGFhoZu0NTU/EKqZktLyzdOTk7bQX4/U1tbu1pcXPwvsZoVFBR+lZeXLwUyz4MMuCMlJbWmv79/o56e3k9Cms3MzL5PmjRphYCAwCYg9wE4MwEZwkBsDsReO3fudN+zZ4/shQsX2ICxA9bEzs7OYGBg8NPHx+eBra3tdqDQVpDLgfgjuEABZk2QS3hBAQvExkBsAHIpMAsLAOP6PzC63gP590FOBmJQCXQPiL8Ai4D/KCUS0CBWIAUqB8SAWAiIQeUgqOIAlY/vgPgVEH8AavyDtUQCSoDc/BqEoQUGLIH9A9mGtUwc8JoJIMAAS9XemfR7crQAAAAASUVORK5CYII="/>
|
||||
{% endset %}
|
||||
{% set text %}
|
||||
{{ '%.0f'|format(collector.totaltime) }} ms
|
||||
{% endset %}
|
||||
{% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %}
|
||||
{% endblock %}
|
||||
|
||||
{% block menu %}
|
||||
<span class="label">
|
||||
<span class="icon"><img src="{{ asset('bundles/webprofiler/images/profiler/time.png') }}" alt="Timeline" /></span>
|
||||
<strong>Timeline</strong>
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<h2>Timeline</h2>
|
||||
|
||||
{% set threshold = app.request.query.get('threshold', 1) %}
|
||||
{% set width = app.request.query.get('width', 990) %}
|
||||
|
||||
<form action="" method="get" style="display: inline">
|
||||
<input type="hidden" name="panel" value="time" />
|
||||
<table>
|
||||
<tr>
|
||||
<th style="width: 20%">Total time</th>
|
||||
<td>{{ '%.0f'|format(collector.totaltime) }} ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Initialization time</th>
|
||||
<td>{{ '%.0f'|format(collector.inittime) }} ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Threshold</th>
|
||||
<td><input type="number" size="3" name="threshold" value="{{ threshold }}" /> ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Width</th>
|
||||
<td><input type="range" name="width" min="0" max="1200" step="10" value="{{ width }}" onChange="drawCanvases(this.value);" /> px</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th> </th>
|
||||
<td><input type="submit" value="refresh" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<h3>
|
||||
Main Request
|
||||
<small>
|
||||
- {{ collector.events.section.totaltime }} ms
|
||||
{% if profile.parent %}
|
||||
- <a href="{{ path('_profiler', { 'token': profile.parent.token }) }}?panel=time&threshold={{ threshold }}&width={{ width }}">parent</a>
|
||||
{% endif %}
|
||||
</small>
|
||||
</h3>
|
||||
|
||||
{% set max = collector.events.section.endtime %}
|
||||
|
||||
{{ _self.display_timeline('timeline_' ~ token, collector.events, threshold, colors, width) }}
|
||||
|
||||
{% if profile.children|length %}
|
||||
{% for child in profile.children %}
|
||||
{% set events = child.getcollector('time').events %}
|
||||
<h3>
|
||||
Sub-request "<a href="{{ path('_profiler', { 'token': child.token }) }}?panel=time&threshold={{ threshold }}&width={{ width }}">{{ child.getcollector('request').requestattributes.get('_controller') }}</a>"
|
||||
<small> - {{ events.section.totaltime }} ms</small>
|
||||
</h3>
|
||||
|
||||
{{ _self.display_timeline('timeline_' ~ child.token, events, threshold, colors, width) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<script type="text/javascript">
|
||||
function drawCanvas(request, max, threshold, width) {
|
||||
var colors = {
|
||||
{% for name, color in colors %}
|
||||
"{{ name }}": "{{ color }}"{{ loop.last ? '' : ', ' }}
|
||||
{% endfor %}
|
||||
};
|
||||
var space = 10.5;
|
||||
var ratio = (width - space * 2) / max;
|
||||
var height = 0;
|
||||
for (i = 0; i < request.events.length; i++) {
|
||||
if (request.events[i].totaltime < threshold) {
|
||||
continue;
|
||||
}
|
||||
|
||||
height = height + 38;
|
||||
}
|
||||
var h = space;
|
||||
var x = request.left * ratio + space;
|
||||
|
||||
var canvas = document.getElementById('timeline_' + request.id);
|
||||
canvas.width = width;
|
||||
|
||||
var context = canvas.getContext("2d");
|
||||
context.textBaseline = "middle";
|
||||
context.lineWidth = 0;
|
||||
|
||||
for (i = 0; i < request.events.length; i++) {
|
||||
var event = request.events[i];
|
||||
|
||||
if (event.totaltime < threshold) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (j = 0; j < event.periods.length; j++) {
|
||||
var period = event.periods[j];
|
||||
|
||||
if ('section.child' == event.name) {
|
||||
context.fillStyle = colors.child_sections;
|
||||
context.fillRect(x + period.begin * ratio, 0, (period.end - period.begin) * ratio, height);
|
||||
} else if ('section' == event.category) {
|
||||
context.beginPath();
|
||||
context.strokeStyle = "#dfdfdf";
|
||||
context.moveTo(x + period.begin * ratio, 0);
|
||||
context.lineTo(x + period.begin * ratio, height);
|
||||
context.moveTo(x + period.end * ratio, 0);
|
||||
context.lineTo(x + period.end * ratio, height);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < request.events.length; i++) {
|
||||
var event = request.events[i];
|
||||
|
||||
if (event.totaltime < threshold || 'section.child' == event.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
h += 8;
|
||||
|
||||
for (j = 0; j < event.periods.length; j++) {
|
||||
var period = event.periods[j];
|
||||
|
||||
if (colors[event.name]) {
|
||||
context.fillStyle = colors[event.name];
|
||||
context.strokeStyle = colors[event.name];
|
||||
} else if (colors[event.category]) {
|
||||
context.fillStyle = colors[event.category];
|
||||
context.strokeStyle = colors[event.category];
|
||||
} else {
|
||||
context.fillStyle = colors.default;
|
||||
context.strokeStyle = colors.default;
|
||||
}
|
||||
|
||||
if ('section' != event.category) {
|
||||
context.fillRect(x + period.begin * ratio, h + 3, 2, 6);
|
||||
context.fillRect(x + period.begin * ratio, h, (period.end - period.begin) * ratio ? (period.end - period.begin) * ratio : 2, 6);
|
||||
} else {
|
||||
context.beginPath();
|
||||
context.moveTo(x + period.begin * ratio, h);
|
||||
context.lineTo(x + period.begin * ratio, h + 11);
|
||||
context.lineTo(x + period.begin * ratio + 8, h);
|
||||
context.lineTo(x + period.begin * ratio, h);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(x + period.end * ratio, h);
|
||||
context.lineTo(x + period.end * ratio, h + 11);
|
||||
context.lineTo(x + period.end * ratio - 8, h);
|
||||
context.lineTo(x + period.end * ratio, h);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(x + period.begin * ratio, h);
|
||||
context.lineTo(x + period.end * ratio, h);
|
||||
context.lineTo(x + period.end * ratio, h + 2);
|
||||
context.lineTo(x + period.begin * ratio, h + 2);
|
||||
context.lineTo(x + period.begin * ratio, h);
|
||||
context.fill();
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
h += 30;
|
||||
|
||||
context.beginPath();
|
||||
context.strokeStyle = "#dfdfdf";
|
||||
context.moveTo(0, h - 10);
|
||||
context.lineTo(width, h - 10);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
h = space;
|
||||
|
||||
for (i = 0; i < request.events.length; i++) {
|
||||
var event = request.events[i];
|
||||
|
||||
if (event.totaltime < threshold || 'section.child' == event.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
context.fillStyle = "#444";
|
||||
context.font = "12px sans-serif";
|
||||
var text = event.name;
|
||||
var ms;
|
||||
if (event.totaltime == 0) {
|
||||
ms = " < 1 ms";
|
||||
} else {
|
||||
ms = " ~ " + event.totaltime + " ms";
|
||||
}
|
||||
if (x + event.starttime * ratio + context.measureText(text + ms).width > width) {
|
||||
context.textAlign = "end";
|
||||
context.font = "10px sans-serif";
|
||||
xc = x + event.endtime * ratio - 1;
|
||||
context.fillText(ms, xc, h);
|
||||
|
||||
xc = xc - context.measureText(ms).width
|
||||
context.font = "12px sans-serif";
|
||||
context.fillText(text, xc, h);
|
||||
} else {
|
||||
context.textAlign = "start";
|
||||
context.font = "12px sans-serif";
|
||||
xc = x + event.starttime * ratio + 1;
|
||||
context.fillText(text, xc, h);
|
||||
|
||||
xc = xc + context.measureText(text).width;
|
||||
context.font = "10px sans-serif";
|
||||
context.fillText(ms, xc, h);
|
||||
}
|
||||
|
||||
h += 38;
|
||||
}
|
||||
}
|
||||
|
||||
function drawCanvases(width)
|
||||
{
|
||||
for (k = 0; k < requests_data.requests.length; k++) {
|
||||
drawCanvas(requests_data.requests[k], requests_data.max, {{ threshold }}, width);
|
||||
}
|
||||
}
|
||||
|
||||
var requests_data = {
|
||||
"max": {{ collector.events.section.endtime }},
|
||||
"requests": [
|
||||
{{ _self.dump_request_data(token, profile, collector.events, collector.events.section.origin) }}
|
||||
|
||||
{% if profile.children|length %}
|
||||
,
|
||||
{% for child in profile.children %}
|
||||
{{ _self.dump_request_data(child.token, child, child.getcollector('time').events, collector.events.section.origin) }}{{ loop.last ? '' : ',' }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
]
|
||||
};
|
||||
|
||||
drawCanvases({{ width }});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% macro dump_request_data(token, profile, events, origin) %}
|
||||
{
|
||||
"id": "{{ token }}",
|
||||
"left": {{ events.section.origin - origin }},
|
||||
"events": [
|
||||
{{ _self.dump_events(events) }}
|
||||
]
|
||||
}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dump_events(events) %}
|
||||
{% for name, event in events %}
|
||||
{% if 'section' != name %}
|
||||
{
|
||||
"name": "{{ name }}",
|
||||
"category": "{{ event.category }}",
|
||||
"origin": {{ event.origin }},
|
||||
"starttime": {{ event.starttime }},
|
||||
"endtime": {{ event.endtime }},
|
||||
"totaltime": {{ event.totaltime }},
|
||||
"periods": [
|
||||
{%- for period in event.periods -%}
|
||||
{"begin": {{ period.0 }}, "end": {{ period.1 }}}{{ loop.last ? '' : ', ' }}
|
||||
{%- endfor -%}
|
||||
]
|
||||
}{{ loop.last ? '' : ',' }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro display_timeline(id, events, threshold, colors, width) %}
|
||||
{% set height = 0 %}
|
||||
{% for name, event in events if 'section' != name and event.totaltime >= threshold %}
|
||||
{% set height = height + 38 %}
|
||||
{% endfor %}
|
||||
|
||||
<div>
|
||||
<small>
|
||||
{% for category, color in colors %}
|
||||
<span style="background-color: {{ color }}"> </span> {{ category }}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<canvas width="{{ width }}" height="{{ height }}" id="{{ id }}" class="timeline"></canvas>
|
||||
{% endmacro %}
|
|
@ -1,11 +0,0 @@
|
|||
{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}
|
||||
|
||||
{% block toolbar %}
|
||||
{% set icon %}
|
||||
<img width="16" height="28" alt="Timers" style="vertical-align: middle; margin-right: 5px;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAcCAYAAABoMT8aAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAiNJREFUeNpi/P//PwMlgImBQjDwBrCcO3cOq0RRUdF3ZH5fXx8nTVzAePbsWcq8gMwxMjJiSUlJcXv9+nXm169fbf78+SMAVsTC8paXl3ePmJjYjJkzZx4GevsviheAGhmBguL+/v4779y5s/Xjx48+MM0gAGQLv3//PvzmzZv7AwMD19y+fVsEpAfsBWBCYly8eLHcsmXLjnz//l2GGGcDXXM1IyPD2dvb+xXIBTwbN25chU3zgQMHwBgdfP78WXvp0qVzgUwuprq6utg3b96YkRp4z549854wYYI7071791LJjYFLly7lM7148UKHXAOALtdnAYYwCyGFyOHg4OAAZ3/69ImfopTIzMz8j4WVlfXf79+/sRqEbBs2wMfH94tJXV39DbkuUFFReclkb29/jlwDPD09jzGFhoZu0NTU/EKqZktLyzdOTk7bQX4/U1tbu1pcXPwvsZoVFBR+lZeXLwUyz4MMuCMlJbWmv79/o56e3k9Cms3MzL5PmjRphYCAwCYg9wE4MwEZwkBsDsReO3fudN+zZ4/shQsX2ICxA9bEzs7OYGBg8NPHx+eBra3tdqDQVpDLgfgjuEABZk2QS3hBAQvExkBsAHIpMAsLAOP6PzC63gP590FOBmJQCXQPiL8Ai4D/KCUS0CBWIAUqB8SAWAiIQeUgqOIAlY/vgPgVEH8AavyDtUQCSoDc/BqEoQUGLIH9A9mGtUwc8JoJIMAAS9XemfR7crQAAAAASUVORK5CYII="/>
|
||||
{% endset %}
|
||||
{% set text %}
|
||||
{{ '%.0f'|format(collector.time * 1000) }} ms
|
||||
{% endset %}
|
||||
{% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': false } %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,110 @@
|
|||
<?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\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* TimeDataCollector.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TimeDataCollector extends DataCollector
|
||||
{
|
||||
protected $kernel;
|
||||
|
||||
public function __construct(KernelInterface $kernel = null)
|
||||
{
|
||||
$this->kernel = $kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collect(Request $request, Response $response, \Exception $exception = null)
|
||||
{
|
||||
$this->data = array(
|
||||
'start_time' => (null !== $this->kernel ? $this->kernel->getStartTime() : $_SERVER['REQUEST_TIME']) * 1000,
|
||||
'events' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request events.
|
||||
*
|
||||
* @param array $event The request events
|
||||
*/
|
||||
public function setEvents(array $events)
|
||||
{
|
||||
foreach ($events as $event) {
|
||||
$event->ensureStopped();
|
||||
}
|
||||
|
||||
$this->data['events'] = $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request events.
|
||||
*
|
||||
* @return array The request events
|
||||
*/
|
||||
public function getEvents()
|
||||
{
|
||||
return $this->data['events'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request elapsed time.
|
||||
*
|
||||
* @return integer The elapsed time
|
||||
*/
|
||||
public function getTotalTime()
|
||||
{
|
||||
$values = array_values($this->data['events']);
|
||||
$lastEvent = $values[count($values) - 1];
|
||||
|
||||
return $lastEvent->getOrigin() + $lastEvent->getEndTime() - $this->data['start_time'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the initialization time.
|
||||
*
|
||||
* This is the time spent until the beginning of the request handling.
|
||||
*
|
||||
* @return integer The elapsed time
|
||||
*/
|
||||
public function getInitTime()
|
||||
{
|
||||
return $this->data['events']['section']->getOrigin() - $this->getStartTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request time.
|
||||
*
|
||||
* @return integer The time
|
||||
*/
|
||||
public function getStartTime()
|
||||
{
|
||||
return $this->data['start_time'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'time';
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
<?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\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* TimerDataCollector.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TimerDataCollector extends DataCollector
|
||||
{
|
||||
protected $kernel;
|
||||
|
||||
public function __construct(KernelInterface $kernel)
|
||||
{
|
||||
$this->kernel = $kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collect(Request $request, Response $response, \Exception $exception = null)
|
||||
{
|
||||
$this->data = array(
|
||||
'time' => microtime(true) - $this->kernel->getStartTime(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request time.
|
||||
*
|
||||
* @return integer The time
|
||||
*/
|
||||
public function getTime()
|
||||
{
|
||||
return $this->data['time'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'timer';
|
||||
}
|
||||
}
|
Reference in New Issue