diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php new file mode 100644 index 0000000000000..3d1908bf9bc92 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php @@ -0,0 +1,50 @@ + + * + * 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 + */ +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))); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 597b2fd678fde..6355908b88688 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -356,6 +356,7 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $loader->load('profiling.xml'); $loader->load('collectors.xml'); + $loader->load('cache_debug.xml'); if ($this->formConfigEnabled) { $loader->load('form_debug.xml'); diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index fac40a6c3ef75..535fa0a5b7caf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -14,6 +14,7 @@ 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 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new CompilerDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new ConfigCachePass()); + $container->addCompilerPass(new CacheCollectorPass()); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml new file mode 100644 index 0000000000000..3d68472028e93 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache_debug.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig new file mode 100644 index 0000000000000..cd096699df799 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig @@ -0,0 +1,146 @@ +{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} + +{% block toolbar %} + {% if collector.totals.calls > 0 %} + {% set icon %} + {{ include('@WebProfiler/Icon/cache.svg') }} + {{ collector.totals.calls }} + + in + {{ '%0.2f'|format(collector.totals.time * 1000) }} + ms + + {% endset %} + {% set text %} +
+ Cache Calls + {{ collector.totals.calls }} +
+
+ Total time + {{ '%0.2f'|format(collector.totals.time * 1000) }} ms +
+
+ Cache hits + {{ collector.totals.hits }}/{{ collector.totals.reads }} ({{ collector.totals['hits/reads'] }}) +
+
+ Cache writes + {{ collector.totals.writes }} +
+ {% endset %} + {% include 'WebProfilerBundle:Profiler:toolbar_item.html.twig' with { 'link': profiler_url } %} + {% endif %} +{% endblock %} + +{% block menu %} + + + {{ include('@WebProfiler/Icon/cache.svg') }} + + Cache + + {{ collector.totals.calls }} + {{ '%0.2f'|format(collector.totals.time * 1000) }} ms + + +{% endblock %} + +{% block panel %} +

Cache

+
+
+ {{ collector.totals.calls }} + Total calls +
+
+ {{ '%0.2f'|format(collector.totals.time * 1000) }} ms + Total time +
+
+ {{ collector.totals.reads }} + Total reads +
+
+ {{ collector.totals.hits }} + Total hits +
+
+ {{ collector.totals.misses }} + Total misses +
+
+ {{ collector.totals.writes }} + Total writes +
+
+ {{ collector.totals.deletes }} + Total deletes +
+
+ {{ collector.totals['hits/reads'] }} + Hits/reads +
+
+ + {% for name, calls in collector.calls %} +

Statistics for '{{ name }}'

+
+ {% for key, value in collector.statistics[name] %} +
+ + {% if key == 'time' %} + {{ '%0.2f'|format(1000*value) }} ms + {% else %} + {{ value }} + {% endif %} + + {{ key|capitalize }} +
+ {% endfor %} +
+

Calls for '{{ name }}'

+ + {% if not collector.totals.calls %} +

+ No calls. +

+ {% else %} + + + + + + + + + {% for i, call in calls %} + + + + + + + + + + + + + + + + + + {% endfor %} + + +
KeyValue
#{{ i }}Pool::{{ call.name }}
Argument{{ profiler_dump(call.argument, maxDepth=2) }}
Results + {% if call.result != false %} + {{ profiler_dump(call.result, maxDepth=1) }} + {% endif %} +
Time{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms
+ {% endif %} + {% endfor %} + +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg new file mode 100644 index 0000000000000..984a4efd4a475 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/cache.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php new file mode 100644 index 0000000000000..200867c0ec7ec --- /dev/null +++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php @@ -0,0 +1,185 @@ + + * + * 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 + * @author Tobias Nyholm + */ +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; + } +}