feature #13428 Added a Twig profiler (fabpot)

This PR was merged into the 2.7 branch.

Discussion
----------

Added a Twig profiler

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

This PR integrates the new Twig 1.18 Profiler (see twigphp/Twig#1597) into Symfony (replace the current TimedTwigEngine) and adds  a new Twig panel.

The timers are now available for all rendered templates (TimedTwigEngine was only able to get information from a few of them -- mainly the first template only).

The Twig panel gives you a lot of information about the execution of the templates, including a call graph.

![image](https://cloud.githubusercontent.com/assets/47313/5773885/fdf6756e-9d67-11e4-8dce-5ec20b07eca9.png)

![image](https://cloud.githubusercontent.com/assets/47313/5773892/0ae24d5c-9d68-11e4-9cbe-767bc31c9152.png)

![image](https://cloud.githubusercontent.com/assets/47313/5773897/13c0b6b6-9d68-11e4-95a1-b9188aca9651.png)

![image](https://cloud.githubusercontent.com/assets/47313/5773902/1c5498d8-9d68-11e4-975e-9822385fb836.png)

![image](https://cloud.githubusercontent.com/assets/47313/5773917/4eba00ba-9d68-11e4-8114-0a2d05eae5ea.png)

Commits
-------

daad64f added a Twig panel to the WebProfiler
ef0c967 integrated the Twig profiler
This commit is contained in:
Fabien Potencier 2015-01-25 19:20:11 +01:00
commit 9e8cb01fde
11 changed files with 312 additions and 35 deletions

View File

@ -18,7 +18,7 @@
"require": {
"php": ">=5.3.9",
"doctrine/common": "~2.3",
"twig/twig": "~1.17",
"twig/twig": "~1.18",
"psr/log": "~1.0"
},
"replace": {

View File

@ -0,0 +1,143 @@
<?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\Bridge\Twig\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* TwigDataCollector.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class TwigDataCollector extends DataCollector implements LateDataCollectorInterface
{
private $profile;
private $computed;
public function __construct(\Twig_Profiler_Profile $profile)
{
$this->profile = $profile;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$this->data['profile'] = serialize($this->profile);
}
public function getTime()
{
return $this->getProfile()->getDuration() * 1000;
}
public function getTemplateCount()
{
return $this->getComputedData('template_count');
}
public function getTemplates()
{
return $this->getComputedData('templates');
}
public function getBlockCount()
{
return $this->getComputedData('block_count');
}
public function getMacroCount()
{
return $this->getComputedData('macro_count');
}
public function getHtmlCallGraph()
{
$dumper = new \Twig_Profiler_Dumper_Html();
return new \Twig_Markup($dumper->dump($this->getProfile()), 'UTF-8');
}
public function getProfile()
{
if (null === $this->profile) {
$this->profile = unserialize($this->data['profile']);
}
return $this->profile;
}
private function getComputedData($index)
{
if (null === $this->computed) {
$this->computed = $this->computeData($this->getProfile());
}
return $this->computed['index'];
}
private function computeData(\Twig_Profiler_Profile $profile)
{
$data = array(
'template_count' => 0,
'block_count' => 0,
'macro_count' => 0,
);
$templates = array();
foreach ($profile as $p) {
$d = $this->computeData($p);
$data['template_count'] += ($p->isTemplate() ? 1 : 0) + $d['template_count'];
$data['block_count'] += ($p->isBlock() ? 1 : 0) + $d['block_count'];
$data['macro_count'] += ($p->isMacro() ? 1 : 0) + $d['macro_count'];
if ($p->isTemplate()) {
if (!isset($templates[$p->getTemplate()])) {
$templates[$p->getTemplate()] = 1;
} else {
$templates[$p->getTemplate()]++;
}
}
foreach ($d['templates'] as $template => $count) {
if (!isset($templates[$template])) {
$templates[$template] = $count;
} else {
$templates[$template] += $count;
}
}
}
$data['templates'] = $templates;
return $data;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'twig';
}
}

View File

@ -0,0 +1,58 @@
<?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\Bridge\Twig\Extension;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class ProfilerExtension extends \Twig_Extension_Profiler
{
private $stopwatch;
private $events;
public function __construct(\Twig_Profiler_Profile $profile, Stopwatch $stopwatch = null)
{
parent::__construct($profile);
$this->stopwatch = $stopwatch;
$this->events = new \SplObjectStorage();
}
public function enter(\Twig_Profiler_Profile $profile)
{
if ($this->stopwatch && $profile->isTemplate()) {
$this->events[$profile] = $this->stopwatch->start($profile->getName(), 'template');
}
parent::enter($profile);
}
public function leave(\Twig_Profiler_Profile $profile)
{
parent::leave($profile);
if ($this->stopwatch && $profile->isTemplate()) {
$this->events[$profile]->stop();
unset($this->events[$profile]);
}
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'native_profiler';
}
}

View File

@ -18,7 +18,7 @@
"require": {
"php": ">=5.3.9",
"symfony/security-csrf": "~2.6|~3.0.0",
"twig/twig": "~1.17"
"twig/twig": "~1.18"
},
"require-dev": {
"symfony/finder": "~2.3|~3.0.0",

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\TwigBundle\Debug;
trigger_error('The '.__NAMESPACE__.'\TimedTwigEngine class is deprecated since version 2.7 and will be removed in 3.0. Use the Twig native profiler instead.', E_USER_DEPRECATED);
use Symfony\Bundle\TwigBundle\TwigEngine;
use Symfony\Component\Templating\TemplateNameParserInterface;
use Symfony\Component\Stopwatch\Stopwatch;
@ -20,6 +22,8 @@ use Symfony\Component\Config\FileLocatorInterface;
* Times the time spent to render a template.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since version 2.7, to be removed in 3.0. Use the Twig native profiler instead.
*/
class TimedTwigEngine extends TwigEngine
{

View File

@ -63,13 +63,13 @@ class ExtensionPass implements CompilerPassInterface
$container->getDefinition('twig.extension.code')->replaceArgument(0, $container->getParameter('templating.helper.code.file_link_format'));
}
if ($container->getParameter('kernel.debug')) {
$container->getDefinition('twig.extension.profiler')->addTag('twig.extension');
$container->getDefinition('twig.extension.debug')->addTag('twig.extension');
}
if ($container->has('templating')) {
$container->getDefinition('twig.cache_warmer')->addTag('kernel.cache_warmer');
if ($container->getParameter('kernel.debug')) {
$container->setDefinition('templating.engine.twig', $container->findDefinition('debug.templating.engine.twig'));
$container->setAlias('debug.templating.engine.twig', 'templating.engine.twig');
}
} else {
$loader = $container->getDefinition('twig.loader.native_filesystem');
$loader->addTag('twig.loader');

View File

@ -101,10 +101,6 @@ class TwigExtension extends Extension
$config['extensions']
);
if ($container->getParameter('kernel.debug')) {
$loader->load('debug.xml');
}
if (isset($config['autoescape_service']) && isset($config['autoescape_service_method'])) {
$config['autoescape'] = array(new Reference($config['autoescape_service']), $config['autoescape_service_method']);
}

View File

@ -1,23 +0,0 @@
<?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="templating.locator" />
<argument type="service" id="debug.stopwatch" />
</service>
<service id="twig.extension.debug" class="Twig_Extension_Debug" public="false">
<tag name="twig.extension" />
</service>
</services>
</container>

View File

@ -70,6 +70,18 @@
<argument type="service" id="templating.locator" />
</service>
<service id="twig.extension.profiler" class="Symfony\Bridge\Twig\Extension\ProfilerExtension" public="false">
<argument type="service" id="twig.profile" />
<argument type="service" id="debug.stopwatch" on-invalid="null" />
</service>
<service id="twig.profile" class="Twig_Profiler_Profile" />
<service id="data_collector.twig" class="Symfony\Bridge\Twig\DataCollector\TwigDataCollector" public="false">
<tag name="data_collector" template="@WebProfiler/Collector/twig.html.twig" id="twig" priority="255" />
<argument type="service" id="twig.profile" />
</service>
<service id="twig.extension.trans" class="%twig.extension.trans.class%" public="false">
<argument type="service" id="translator" />
</service>
@ -121,6 +133,8 @@
<argument type="service" id="twig.form.renderer" />
</service>
<service id="twig.extension.debug" class="Twig_Extension_Debug" public="false" />
<service id="twig.form.engine" class="%twig.form.engine.class%" public="false">
<argument>%twig.form.resources%</argument>
</service>

View File

@ -0,0 +1,85 @@
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set time = collector.templatecount ? '%0.0f ms'|format(collector.time) : 'n/a' %}
{% set icon %}
<img height="28" alt="Twig" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAcCAYAAACOGPReAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbElEQVRIx2NgGAXUBowMDAwMaWlp/6ll4KxZsxhZYJy0tDRqGMjAwMDAwEQL77OgCxSXlJBsSG9PDwqfJi6lj/fRvTJ4XYocUTBXE4q8oRtRRBnKwsw8RFw6fA0lKkd1dnYOIpfCCthRMIIAAI0IFu9Hxh7ZAAAAAElFTkSuQmCC" />
<span class="sf-toolbar-status">{{ time }}</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Render Time</b>
<span>{{ time }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Template Calls</b>
<span>{{ collector.templatecount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Block Calls</b>
<span>{{ collector.blockcount }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Macro Calls</b>
<span>{{ collector.macrocount }}</span>
</div>
{% endset %}
{% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %}
{% endblock %}
{% block menu %}
<span class="label">
<span class="icon"><img alt="Twig" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAgCAYAAAABtRhCAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAbklEQVRIx2NgGAVDHTDCGGlpaf9pZcmsWbPg9rAgS6SlpdHCMhQ+E72DlAWbYHFJCcUG9/b0YBWnuw9HLaRPosEV4cPHh9iyBczXxGaZ0WxBfBwwM4/mw1ELRy0c4MK7s7NzCPsQvYU1CkYBNgAAV5UW+fU+ZL4AAAAASUVORK5CYII="></span>
<strong>Twig</strong>
<span class="count">
<span>{{ collector.templatecount }}</span>
<span>{{ '%0.0f ms'|format(collector.time) }}</span>
</span>
</span>
{% endblock %}
{% block panel %}
{% if collector.templatecount %}
<h2>Twig Stats</h2>
<table>
<tr>
<th>Total Render Time<br /><small>including sub-requests rendering time</small></th>
<td><pre>{{ '%0.0f ms'|format(collector.time) }}</pre></td>
</tr>
<tr>
<th scope="col" style="width: 30%">Template Calls</th>
<td scope="col" style="width: 60%"><pre>{{ collector.templatecount }}</pre></td>
</tr>
<tr>
<th>Block Calls</th>
<td><pre>{{ collector.blockcount }}</pre></td>
</tr>
<tr>
<th>Macro Calls</th>
<td><pre>{{ collector.macrocount }}</pre></td>
</tr>
</table>
<h2>Rendered Templates</h2>
<table>
<tr>
<th scope="col">Template Name</th>
<th scope="col">Render Count</th>
</tr>
{% for template, count in collector.templates %}
<tr>
<td><code>{{ template }}</code></td>
<td><pre>{{ count }}</pre></td>
</tr>
{% endfor %}
</table>
<h2>Rendering Call Graph</h2>
{{ collector.htmlcallgraph }}
{% else %}
<p><em>No Twig templates were rendered for this request.</em></p>
{% endif %}
{% endblock %}

View File

@ -19,7 +19,7 @@
"php": ">=5.3.9",
"symfony/http-kernel": "~2.4|~3.0.0",
"symfony/routing": "~2.2|~3.0.0",
"symfony/twig-bridge": "~2.2|~3.0.0"
"symfony/twig-bridge": "~2.7|~3.0.0"
},
"require-dev": {
"symfony/config": "~2.2|~3.0.0",