Skip to content

[HttpKernel][WebProfilerBundle] Add session profiling #36364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@
->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])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
'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'),
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ public function testNullSessionHandler()
$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()));
}

Expand Down Expand Up @@ -1312,7 +1312,7 @@ public function testSessionCookieSecureAuto()
{
$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()));
}

Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

5.2.0
-----

* added session usage

5.0.0
-----

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%}
Expand Down Expand Up @@ -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>
Expand All @@ -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>

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
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;
Expand All @@ -28,10 +31,13 @@
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;
}

/**
Expand Down Expand Up @@ -105,6 +111,8 @@ public function collect(Request $request, Response $response, \Throwable $except
'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',
Expand Down Expand Up @@ -175,6 +183,7 @@ public function reset()
{
$this->data = [];
$this->controllers = new \SplObjectStorage();
$this->sessionUsages = [];
}

public function getMethod()
Expand Down Expand Up @@ -242,6 +251,16 @@ public function getSessionAttributes()
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();
Expand Down Expand Up @@ -382,6 +401,37 @@ public function getName()
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ public function onSessionUsage(): void
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
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;
Expand Down Expand Up @@ -248,6 +251,65 @@ public function testItCollectsTheRedirectionAndClearTheCookie()
$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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -260,9 +261,13 @@ public function testSessionUsageCallbackWhenDebugAndStateless()
$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();
Expand All @@ -277,12 +282,16 @@ public function testSessionUsageCallbackWhenNoDebug()
$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();
}
Expand Down