added Stopwatch support in debug mode, added a timeline representing the stopwatch events in the web profiler

Enjoy!
This commit is contained in:
Fabien Potencier 2011-10-17 10:27:10 +02:00
parent 106ebdbe18
commit 842ac36f33
21 changed files with 715 additions and 80 deletions

View File

@ -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.
*

View File

@ -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">

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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'));

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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',

View File

@ -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>

View File

@ -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.

View File

@ -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

View File

@ -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>&nbsp;</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 }}">&nbsp;&nbsp;&nbsp;</span> {{ category }}&nbsp;&nbsp;
{% endfor %}
</small>
</div>
<canvas width="{{ width }}" height="{{ height }}" id="{{ id }}" class="timeline"></canvas>
{% endmacro %}

View File

@ -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 %}

View File

@ -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';
}
}

View File

@ -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';
}
}