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:
commit
e823e85a7c
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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 %}
|
@ -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 |
185
src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php
Normal file
185
src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user