feature #36364 [HttpKernel][WebProfilerBundle] Add session profiling (mtarld)
This PR was merged into the 5.2-dev branch.
Discussion
----------
[HttpKernel][WebProfilerBundle] Add session profiling
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| License | MIT
| Doc PR |
This PR proposes to add session profiling.
It provides stateless checking status and session usage backtraces.
Under are screesnhots of provided profiling:
![Screenshot from 2020-04-06 13-42-41](https://user-images.githubusercontent.com/4955509/78581189-d6c32580-7833-11ea-9de5-d1e4f8e60c27.png)
![Screenshot from 2020-04-06 13-43-04](https://user-images.githubusercontent.com/4955509/78581193-d88ce900-7833-11ea-90a4-85d07c64d47e.png)
![Screenshot from 2020-04-06 17-43-17](https://user-images.githubusercontent.com/4955509/78581159-cca12700-7833-11ea-98d2-38306ec9ea37.png)
![Screenshot from 2020-04-06 17-43-35](https://user-images.githubusercontent.com/4955509/78581238-e8a4c880-7833-11ea-89e2-ff4fdea8dce5.png)
Commits
-------
5dbaef8883
Add session profiling
This commit is contained in:
commit
8cc90b9845
@ -29,9 +29,16 @@ return static function (ContainerConfigurator $container) {
|
||||
->tag('data_collector', ['template' => '@WebProfiler/Collector/config.html.twig', 'id' => 'config', 'priority' => -255])
|
||||
|
||||
->set('data_collector.request', RequestDataCollector::class)
|
||||
->args([
|
||||
service('request_stack')->ignoreOnInvalid(),
|
||||
])
|
||||
->tag('kernel.event_subscriber')
|
||||
->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335])
|
||||
|
||||
->set('data_collector.request.session_collector', \Closure::class)
|
||||
->factory([\Closure::class, 'fromCallable'])
|
||||
->args([[service('data_collector.request'), 'collectSessionUsage']])
|
||||
|
||||
->set('data_collector.ajax', AjaxDataCollector::class)
|
||||
->tag('data_collector', ['template' => '@WebProfiler/Collector/ajax.html.twig', 'id' => 'ajax', 'priority' => 315])
|
||||
|
||||
|
@ -97,6 +97,7 @@ return static function (ContainerConfigurator $container) {
|
||||
'session' => service('session')->ignoreOnInvalid(),
|
||||
'initialized_session' => service('session')->ignoreOnUninitialized(),
|
||||
'logger' => service('logger')->ignoreOnInvalid(),
|
||||
'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(),
|
||||
]),
|
||||
param('kernel.debug'),
|
||||
])
|
||||
|
@ -504,7 +504,7 @@ abstract class FrameworkExtensionTest extends TestCase
|
||||
$this->assertNull($container->getDefinition('session.storage.native')->getArgument(1));
|
||||
$this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0));
|
||||
|
||||
$expected = ['session', 'initialized_session', 'logger'];
|
||||
$expected = ['session', 'initialized_session', 'logger', 'session_collector'];
|
||||
$this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues()));
|
||||
}
|
||||
|
||||
@ -1312,7 +1312,7 @@ abstract class FrameworkExtensionTest extends TestCase
|
||||
{
|
||||
$container = $this->createContainerFromFile('session_cookie_secure_auto');
|
||||
|
||||
$expected = ['session', 'initialized_session', 'logger', 'session_storage', 'request_stack'];
|
||||
$expected = ['session', 'initialized_session', 'logger', 'session_collector', 'session_storage', 'request_stack'];
|
||||
$this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues()));
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added session usage
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
|
@ -59,6 +59,11 @@
|
||||
<b>Has session</b>
|
||||
<span>{% if collector.sessionmetadata|length %}yes{% else %}no{% endif %}</span>
|
||||
</div>
|
||||
|
||||
<div class="sf-toolbar-info-piece">
|
||||
<b>Stateless Check</b>
|
||||
<span>{% if collector.statelesscheck %}yes{% else %}no{% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if redirect_handler is defined -%}
|
||||
@ -228,7 +233,7 @@
|
||||
</div>
|
||||
|
||||
<div class="tab {{ collector.sessionmetadata is empty ? 'disabled' }}">
|
||||
<h3 class="tab-title">Session</h3>
|
||||
<h3 class="tab-title">Session{% if collector.sessionusages is not empty %} <span class="badge">{{ collector.sessionusages|length }}</span>{% endif %}</h3>
|
||||
|
||||
<div class="tab-content">
|
||||
<h3>Session Metadata</h3>
|
||||
@ -250,6 +255,54 @@
|
||||
{% else %}
|
||||
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionattributes, labels: ['Attribute', 'Value'] }, with_context = false) }}
|
||||
{% endif %}
|
||||
|
||||
<h3>Session Usage</h3>
|
||||
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<span class="value">{{ collector.sessionusages|length }}</span>
|
||||
<span class="label">Usages</span>
|
||||
</div>
|
||||
|
||||
<div class="metric">
|
||||
<span class="value">{{ include('@WebProfiler/Icon/' ~ (collector.statelesscheck ? 'yes' : 'no') ~ '.svg') }}</span>
|
||||
<span class="label">Stateless check enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if collector.sessionusages is empty %}
|
||||
<div class="empty">
|
||||
<p>Session not used.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<table class="session_usages">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="full-width">Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for key, usage in collector.sessionusages %}
|
||||
<tr>
|
||||
<td class="font-normal">
|
||||
{%- set link = usage.file|file_link(usage.line) %}
|
||||
{%- if link %}<a href="{{ link }}" title="{{ usage.name }}">{% else %}<span title="{{ usage.name }}">{% endif %}
|
||||
{{ usage.name }}
|
||||
{%- if link %}</a>{% else %}</span>{% endif %}
|
||||
<div class="text-small font-normal">
|
||||
{% set usage_id = 'session-usage-trace-' ~ key %}
|
||||
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ usage_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
|
||||
</div>
|
||||
<div id="{{ usage_id }}" class="context sf-toggle-content sf-toggle-hidden">
|
||||
{{ profiler_dump(usage.trace, maxDepth=2) }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -4,6 +4,7 @@ CHANGELOG
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added session usage
|
||||
* made the public `http_cache` service handle requests when available
|
||||
* allowed enabling trusted hosts and proxies using new `kernel.trusted_hosts`,
|
||||
`kernel.trusted_proxies` and `kernel.trusted_headers` parameters
|
||||
|
@ -15,7 +15,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
@ -28,10 +31,13 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
class RequestDataCollector extends DataCollector implements EventSubscriberInterface, LateDataCollectorInterface
|
||||
{
|
||||
protected $controllers;
|
||||
private $sessionUsages = [];
|
||||
private $requestStack;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(?RequestStack $requestStack = null)
|
||||
{
|
||||
$this->controllers = new \SplObjectStorage();
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,6 +111,8 @@ class RequestDataCollector extends DataCollector implements EventSubscriberInter
|
||||
'response_cookies' => $responseCookies,
|
||||
'session_metadata' => $sessionMetadata,
|
||||
'session_attributes' => $sessionAttributes,
|
||||
'session_usages' => array_values($this->sessionUsages),
|
||||
'stateless_check' => $this->requestStack && $this->requestStack->getMasterRequest()->attributes->get('_stateless', false),
|
||||
'flashes' => $flashes,
|
||||
'path_info' => $request->getPathInfo(),
|
||||
'controller' => 'n/a',
|
||||
@ -175,6 +183,7 @@ class RequestDataCollector extends DataCollector implements EventSubscriberInter
|
||||
{
|
||||
$this->data = [];
|
||||
$this->controllers = new \SplObjectStorage();
|
||||
$this->sessionUsages = [];
|
||||
}
|
||||
|
||||
public function getMethod()
|
||||
@ -242,6 +251,16 @@ class RequestDataCollector extends DataCollector implements EventSubscriberInter
|
||||
return $this->data['session_attributes']->getValue();
|
||||
}
|
||||
|
||||
public function getStatelessCheck()
|
||||
{
|
||||
return $this->data['stateless_check'];
|
||||
}
|
||||
|
||||
public function getSessionUsages()
|
||||
{
|
||||
return $this->data['session_usages'];
|
||||
}
|
||||
|
||||
public function getFlashes()
|
||||
{
|
||||
return $this->data['flashes']->getValue();
|
||||
@ -382,6 +401,37 @@ class RequestDataCollector extends DataCollector implements EventSubscriberInter
|
||||
return 'request';
|
||||
}
|
||||
|
||||
public function collectSessionUsage(): void
|
||||
{
|
||||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
|
||||
$traceEndIndex = \count($trace) - 1;
|
||||
for ($i = $traceEndIndex; $i > 0; --$i) {
|
||||
if (null !== ($class = $trace[$i]['class'] ?? null) && (is_subclass_of($class, SessionInterface::class) || is_subclass_of($class, SessionBagInterface::class))) {
|
||||
$traceEndIndex = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((\count($trace) - 1) === $traceEndIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove part of the backtrace that belongs to session only
|
||||
array_splice($trace, 0, $traceEndIndex);
|
||||
|
||||
// Merge identical backtraces generated by internal call reports
|
||||
$name = sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']);
|
||||
if (!\array_key_exists($name, $this->sessionUsages)) {
|
||||
$this->sessionUsages[$name] = [
|
||||
'name' => $name,
|
||||
'file' => $trace[0]['file'],
|
||||
'line' => $trace[0]['line'],
|
||||
'trace' => $trace,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a controller.
|
||||
*
|
||||
|
@ -152,6 +152,10 @@ abstract class AbstractSessionListener implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->container && $this->container->has('session_collector')) {
|
||||
$this->container->get('session_collector')();
|
||||
}
|
||||
|
||||
if (!$requestStack = $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) {
|
||||
return;
|
||||
}
|
||||
|
@ -17,8 +17,11 @@ use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
|
||||
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
|
||||
@ -248,6 +251,65 @@ class RequestDataCollectorTest extends TestCase
|
||||
$this->assertNull($cookie->getValue());
|
||||
}
|
||||
|
||||
public function testItCollectsTheSessionTraceProperly()
|
||||
{
|
||||
$collector = new RequestDataCollector();
|
||||
$request = $this->createRequest();
|
||||
|
||||
// RequestDataCollectorTest doesn't implement SessionInterface or SessionBagInterface, therefore should do nothing.
|
||||
$collector->collectSessionUsage();
|
||||
|
||||
$collector->collect($request, $this->createResponse());
|
||||
$this->assertSame([], $collector->getSessionUsages());
|
||||
|
||||
$collector->reset();
|
||||
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->method('getMetadataBag')->willReturnCallback(static function () use ($collector) {
|
||||
$collector->collectSessionUsage();
|
||||
});
|
||||
$session->getMetadataBag();
|
||||
|
||||
$collector->collect($request, $this->createResponse());
|
||||
$collector->lateCollect();
|
||||
|
||||
$usages = $collector->getSessionUsages();
|
||||
|
||||
$this->assertCount(1, $usages);
|
||||
$this->assertSame(__FILE__, $usages[0]['file']);
|
||||
$this->assertSame(__LINE__ - 9, $line = $usages[0]['line']);
|
||||
|
||||
$trace = $usages[0]['trace'];
|
||||
$this->assertSame('getMetadataBag', $trace[0]['function']);
|
||||
$this->assertSame(self::class, $class = $trace[1]['class']);
|
||||
|
||||
$this->assertSame(sprintf('%s:%s', $class, $line), $usages[0]['name']);
|
||||
}
|
||||
|
||||
public function testStatelessCheck()
|
||||
{
|
||||
$requestStack = new RequestStack();
|
||||
$request = $this->createRequest();
|
||||
$requestStack->push($request);
|
||||
|
||||
$collector = new RequestDataCollector($requestStack);
|
||||
$collector->collect($request, $response = $this->createResponse());
|
||||
$collector->lateCollect();
|
||||
|
||||
$this->assertFalse($collector->getStatelessCheck());
|
||||
|
||||
$requestStack = new RequestStack();
|
||||
$request = $this->createRequest();
|
||||
$request->attributes->set('_stateless', true);
|
||||
$requestStack->push($request);
|
||||
|
||||
$collector = new RequestDataCollector($requestStack);
|
||||
$collector->collect($request, $response = $this->createResponse());
|
||||
$collector->lateCollect();
|
||||
|
||||
$this->assertTrue($collector->getStatelessCheck());
|
||||
}
|
||||
|
||||
protected function createRequest($routeParams = ['name' => 'foo'])
|
||||
{
|
||||
$request = Request::create('http://test.com/foo?bar=baz');
|
||||
|
@ -20,6 +20,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
|
||||
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
@ -260,9 +261,13 @@ class SessionListenerTest extends TestCase
|
||||
$requestStack->push($request);
|
||||
$requestStack->push(new Request());
|
||||
|
||||
$collector = $this->createMock(RequestDataCollector::class);
|
||||
$collector->expects($this->once())->method('collectSessionUsage');
|
||||
|
||||
$container = new Container();
|
||||
$container->set('initialized_session', $session);
|
||||
$container->set('request_stack', $requestStack);
|
||||
$container->set('session_collector', \Closure::fromCallable([$collector, 'collectSessionUsage']));
|
||||
|
||||
$this->expectException(UnexpectedSessionUsageException::class);
|
||||
(new SessionListener($container, true))->onSessionUsage();
|
||||
@ -277,12 +282,16 @@ class SessionListenerTest extends TestCase
|
||||
$request = new Request();
|
||||
$request->attributes->set('_stateless', true);
|
||||
|
||||
$requestStack = $this->getMockBuilder(RequestStack::class)->getMock();
|
||||
$requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request);
|
||||
$requestStack = new RequestStack();
|
||||
$requestStack->push($request);
|
||||
|
||||
$collector = $this->createMock(RequestDataCollector::class);
|
||||
$collector->expects($this->never())->method('collectSessionUsage');
|
||||
|
||||
$container = new Container();
|
||||
$container->set('initialized_session', $session);
|
||||
$container->set('request_stack', $requestStack);
|
||||
$container->set('session_collector', $collector);
|
||||
|
||||
(new SessionListener($container))->onSessionUsage();
|
||||
}
|
||||
|
Reference in New Issue
Block a user