feature #21065 Added cache data collector and profiler page (Nyholm)

This PR was squashed before being merged into the 3.3-dev branch (closes #21065).

Discussion
----------

Added cache data collector and profiler page

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

Adding a first version of cache profiler page. Most things are taken from PHP-cache.
FYI: @aequasi

### What is included?

A collector, recording adapter and a profiler page.

![screen shot 2016-12-27 at 16 07 35](https://cloud.githubusercontent.com/assets/1275206/21502325/4bee2ed4-cc4f-11e6-89fc-37ed16aca864.png)
![screen shot 2016-12-27 at 16 07 45](https://cloud.githubusercontent.com/assets/1275206/21502326/4bee9450-cc4f-11e6-904d-527b7b0ce85b.png)

### What is not included?

* A good logo
* Nice design on the profiler page

This PR aims to pass as the minimum requirement for a cache page. Im happy to do fancy extra features but those should be a different PR.

Commits
-------

7497f1c6b6 Added cache data collector and profiler page
This commit is contained in:
Fabien Potencier 2017-01-17 21:46:26 -08:00
commit e823e85a7c
7 changed files with 400 additions and 0 deletions

View File

@ -0,0 +1,50 @@
<?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\DependencyInjection\Compiler;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Inject a data collector to all the cache services to be able to get detailed statistics.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class CacheCollectorPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('data_collector.cache')) {
return;
}
$collectorDefinition = $container->getDefinition('data_collector.cache');
foreach ($container->findTaggedServiceIds('cache.pool') as $id => $attributes) {
if ($container->getDefinition($id)->isAbstract()) {
continue;
}
$container->register($id.'.recorder', TraceableAdapter::class)
->setDecoratedService($id)
->addArgument(new Reference($id.'.recorder.inner'))
->setPublic(false);
// Tell the collector to add the new instance
$collectorDefinition->addMethodCall('addInstance', array($id, new Reference($id)));
}
}
}

View File

@ -356,6 +356,7 @@ class FrameworkExtension extends Extension
$loader->load('profiling.xml');
$loader->load('collectors.xml');
$loader->load('cache_debug.xml');
if ($this->formConfigEnabled) {
$loader->load('form_debug.xml');

View File

@ -14,6 +14,7 @@ namespace Symfony\Bundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProcessorPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CacheCollectorPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolClearerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ControllerArgumentValueResolverPass;
@ -105,6 +106,7 @@ class FrameworkBundle extends Bundle
$container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new CompilerDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new ConfigCachePass());
$container->addCompilerPass(new CacheCollectorPass());
}
}
}

View File

@ -0,0 +1,13 @@
<?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">
<services>
<!-- DataCollector -->
<service id="data_collector.cache" class="Symfony\Component\Cache\DataCollector\CacheDataCollector">
<tag name="data_collector" template="@WebProfiler/Collector/cache.html.twig" id="cache" priority="275" />
</service>
</services>
</container>

View File

@ -0,0 +1,146 @@
{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}
{% block toolbar %}
{% if collector.totals.calls > 0 %}
{% set icon %}
{{ include('@WebProfiler/Icon/cache.svg') }}
<span class="sf-toolbar-value">{{ collector.totals.calls }}</span>
<span class="sf-toolbar-info-piece-additional-detail">
<span class="sf-toolbar-label">in</span>
<span class="sf-toolbar-value">{{ '%0.2f'|format(collector.totals.time * 1000) }}</span>
<span class="sf-toolbar-label">ms</span>
</span>
{% endset %}
{% set text %}
<div class="sf-toolbar-info-piece">
<b>Cache Calls</b>
<span>{{ collector.totals.calls }}</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Total time</b>
<span>{{ '%0.2f'|format(collector.totals.time * 1000) }} ms</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache hits</b>
<span>{{ collector.totals.hits }}/{{ collector.totals.reads }} ({{ collector.totals['hits/reads'] }})</span>
</div>
<div class="sf-toolbar-info-piece">
<b>Cache writes</b>
<span>{{ collector.totals.writes }}</span>
</div>
{% endset %}
{% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %}
{% endif %}
{% endblock %}
{% block menu %}
<span class="label {{ collector.totals.calls == 0 ? 'disabled' }}">
<span class="icon">
{{ include('@WebProfiler/Icon/cache.svg') }}
</span>
<strong>Cache</strong>
<span class="count">
<span>{{ collector.totals.calls }}</span>
<span>{{ '%0.2f'|format(collector.totals.time * 1000) }} ms</span>
</span>
</span>
{% endblock %}
{% block panel %}
<h2>Cache</h2>
<div class="metrics">
<div class="metric">
<span class="value">{{ collector.totals.calls }}</span>
<span class="label">Total calls</span>
</div>
<div class="metric">
<span class="value">{{ '%0.2f'|format(collector.totals.time * 1000) }} ms</span>
<span class="label">Total time</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.reads }}</span>
<span class="label">Total reads</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.hits }}</span>
<span class="label">Total hits</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.misses }}</span>
<span class="label">Total misses</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.writes }}</span>
<span class="label">Total writes</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals.deletes }}</span>
<span class="label">Total deletes</span>
</div>
<div class="metric">
<span class="value">{{ collector.totals['hits/reads'] }}</span>
<span class="label">Hits/reads</span>
</div>
</div>
{% for name, calls in collector.calls %}
<h3>Statistics for '{{ name }}'</h3>
<div class="metrics">
{% for key, value in collector.statistics[name] %}
<div class="metric">
<span class="value">
{% if key == 'time' %}
{{ '%0.2f'|format(1000*value) }} ms
{% else %}
{{ value }}
{% endif %}
</span>
<span class="label">{{ key|capitalize }}</span>
</div>
{% endfor %}
</div>
<h4>Calls for '{{ name }}'</h4>
{% if not collector.totals.calls %}
<p>
<em>No calls.</em>
</p>
{% else %}
<table>
<thead>
<tr>
<th style="width: 5rem;">Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for i, call in calls %}
<tr>
<th style="padding-top:2rem">#{{ i }}</th>
<th style="padding-top:2rem">Pool::{{ call.name }}</th>
</tr>
<tr>
<th>Argument</th>
<td>{{ profiler_dump(call.argument, maxDepth=2) }}</td>
</tr>
<tr>
<th>Results</th>
<td>
{% if call.result != false %}
{{ profiler_dump(call.result, maxDepth=1) }}
{% endif %}
</td>
</tr>
<tr>
<th>Time</th>
<td>{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="#AAA" d="M2.26 6.09l9.06-4.67a1.49 1.49 0 0 1 1.37 0l9.06 4.67a1.49 1.49 0 0 1 0 2.65l-9.06 4.67a1.49 1.49 0 0 1-1.37 0L2.26 8.74a1.49 1.49 0 0 1 0-2.65zM20.55 11L12 15.39 3.45 11a1.36 1.36 0 0 0-1.25 2.42l9.17 4.73a1.36 1.36 0 0 0 1.25 0l9.17-4.73A1.36 1.36 0 0 0 20.55 11zm0 4.47L12 19.86l-8.55-4.41a1.36 1.36 0 0 0-1.25 2.42l9.17 4.73a1.36 1.36 0 0 0 1.25 0l9.17-4.73a1.36 1.36 0 0 0-1.25-2.42z"/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@ -0,0 +1,185 @@
<?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\Cache\DataCollector;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableAdapterEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
/**
* @author Aaron Scherer <aequasi@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class CacheDataCollector extends DataCollector
{
/**
* @var TraceableAdapter[]
*/
private $instances = array();
/**
* @param string $name
* @param TraceableAdapter $instance
*/
public function addInstance($name, TraceableAdapter $instance)
{
$this->instances[$name] = $instance;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
$empty = array('calls' => array(), 'config' => array(), 'options' => array(), 'statistics' => array());
$this->data = array('instances' => $empty, 'total' => $empty);
foreach ($this->instances as $name => $instance) {
$calls = $instance->getCalls();
foreach ($calls as $call) {
if (isset($call->result)) {
$call->result = $this->cloneVar($call->result);
}
if (isset($call->argument)) {
$call->argument = $this->cloneVar($call->argument);
}
}
$this->data['instances']['calls'][$name] = $calls;
}
$this->data['instances']['statistics'] = $this->calculateStatistics();
$this->data['total']['statistics'] = $this->calculateTotalStatistics();
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'cache';
}
/**
* Method returns amount of logged Cache reads: "get" calls.
*
* @return array
*/
public function getStatistics()
{
return $this->data['instances']['statistics'];
}
/**
* Method returns the statistic totals.
*
* @return array
*/
public function getTotals()
{
return $this->data['total']['statistics'];
}
/**
* Method returns all logged Cache call objects.
*
* @return mixed
*/
public function getCalls()
{
return $this->data['instances']['calls'];
}
/**
* @return array
*/
private function calculateStatistics()
{
$statistics = array();
foreach ($this->data['instances']['calls'] as $name => $calls) {
$statistics[$name] = array(
'calls' => 0,
'time' => 0,
'reads' => 0,
'hits' => 0,
'misses' => 0,
'writes' => 0,
'deletes' => 0,
);
/** @var TraceableAdapterEvent $call */
foreach ($calls as $call) {
$statistics[$name]['calls'] += 1;
$statistics[$name]['time'] += $call->end - $call->start;
if ('getItem' === $call->name) {
$statistics[$name]['reads'] += 1;
if ($call->hits) {
$statistics[$name]['hits'] += 1;
} else {
$statistics[$name]['misses'] += 1;
}
} elseif ('getItems' === $call->name) {
$count = $call->hits + $call->misses;
$statistics[$name]['reads'] += $count;
$statistics[$name]['hits'] += $call->hits;
$statistics[$name]['misses'] += $count - $call->misses;
} elseif ('hasItem' === $call->name) {
$statistics[$name]['reads'] += 1;
if (false === $call->result->getRawData()[0][0]) {
$statistics[$name]['misses'] += 1;
} else {
$statistics[$name]['hits'] += 1;
}
} elseif ('save' === $call->name) {
$statistics[$name]['writes'] += 1;
} elseif ('deleteItem' === $call->name) {
$statistics[$name]['deletes'] += 1;
}
}
if ($statistics[$name]['reads']) {
$statistics[$name]['hits/reads'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2).'%';
} else {
$statistics[$name]['hits/reads'] = 'N/A';
}
}
return $statistics;
}
/**
* @return array
*/
private function calculateTotalStatistics()
{
$statistics = $this->getStatistics();
$totals = array(
'calls' => 0,
'time' => 0,
'reads' => 0,
'hits' => 0,
'misses' => 0,
'writes' => 0,
'deletes' => 0,
);
foreach ($statistics as $name => $values) {
foreach ($totals as $key => $value) {
$totals[$key] += $statistics[$name][$key];
}
}
if ($totals['reads']) {
$totals['hits/reads'] = round(100 * $totals['hits'] / $totals['reads'], 2).'%';
} else {
$totals['hits/reads'] = 'N/A';
}
return $totals;
}
}