Skip to content

Commit b6a4950

Browse files
committed
Add session profiling
1 parent a184e54 commit b6a4950

File tree

10 files changed

+199
-19
lines changed

10 files changed

+199
-19
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
->tag('data_collector', ['template' => '@WebProfiler/Collector/config.html.twig', 'id' => 'config', 'priority' => -255])
3030

3131
->set('data_collector.request', RequestDataCollector::class)
32+
->args([
33+
service('request_stack')->ignoreOnInvalid(),
34+
])
3235
->tag('kernel.event_subscriber')
3336
->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335])
3437

src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
'session' => service('session')->ignoreOnInvalid(),
9898
'initialized_session' => service('session')->ignoreOnUninitialized(),
9999
'logger' => service('logger')->ignoreOnInvalid(),
100+
'data_collector.request' => service('data_collector.request')->ignoreOnInvalid(),
100101
]),
101102
param('kernel.debug'),
102103
])

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ public function testNullSessionHandler()
504504
$this->assertNull($container->getDefinition('session.storage.native')->getArgument(1));
505505
$this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0));
506506

507-
$expected = ['session', 'initialized_session', 'logger'];
507+
$expected = ['session', 'initialized_session', 'logger', 'data_collector.request'];
508508
$this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues()));
509509
}
510510

@@ -1312,7 +1312,7 @@ public function testSessionCookieSecureAuto()
13121312
{
13131313
$container = $this->createContainerFromFile('session_cookie_secure_auto');
13141314

1315-
$expected = ['session', 'initialized_session', 'logger', 'session_storage', 'request_stack'];
1315+
$expected = ['session', 'initialized_session', 'logger', 'data_collector.request', 'session_storage', 'request_stack'];
13161316
$this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues()));
13171317
}
13181318

src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.2.0
5+
-----
6+
7+
* added session usage profiling
8+
49
5.0.0
510
-----
611

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/request.html.twig

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
<b>Has session</b>
6060
<span>{% if collector.sessionmetadata|length %}yes{% else %}no{% endif %}</span>
6161
</div>
62+
63+
<div class="sf-toolbar-info-piece">
64+
<b>Stateless check</b>
65+
<span>{% if collector.statelesscheck %}yes{% else %}no{% endif %}</span>
66+
</div>
6267
</div>
6368

6469
{% if redirect_handler is defined -%}
@@ -228,7 +233,7 @@
228233
</div>
229234

230235
<div class="tab {{ collector.sessionmetadata is empty ? 'disabled' }}">
231-
<h3 class="tab-title">Session</h3>
236+
<h3 class="tab-title">Session{% if collector.sessionusages is not empty %} <span class="badge">{{ collector.sessionusages|length }}</span>{% endif %}</h3>
232237

233238
<div class="tab-content">
234239
<h3>Session Metadata</h3>
@@ -250,6 +255,59 @@
250255
{% else %}
251256
{{ include('@WebProfiler/Profiler/table.html.twig', { data: collector.sessionattributes, labels: ['Attribute', 'Value'] }, with_context = false) }}
252257
{% endif %}
258+
259+
<h3>Session Usage</h3>
260+
261+
<div class="metrics">
262+
<div class="metric">
263+
<span class="value">{{ collector.sessionusages|length }}</span>
264+
<span class="label">Usages</span>
265+
</div>
266+
267+
<div class="metric">
268+
<span class="value">{{ include('@WebProfiler/Icon/' ~ (collector.statelesscheck ? 'yes' : 'no') ~ '.svg') }}</span>
269+
<span class="label">Stateless check enabled</span>
270+
</div>
271+
</div>
272+
273+
{% if collector.sessionusages is empty %}
274+
<div class="empty">
275+
<p>Session not used.</p>
276+
</div>
277+
{% else %}
278+
<table class="session_usages">
279+
<thead>
280+
<tr>
281+
<th>Time</th>
282+
<th class="full-width">Usage</th>
283+
</tr>
284+
</thead>
285+
286+
<tbody>
287+
{% for key, usage in collector.sessionusages %}
288+
<tr>
289+
<td class="font-normal text-small" nowrap>
290+
<time class="text-muted newline" title="{{ usage.timestamp|date('r') }}" datetime="{{ usage.timestamp|date('c') }}">{{ usage.timestamp|date('H:i:s') }}</time>
291+
</td>
292+
293+
<td class="font-normal">
294+
{%- set link = usage.file|file_link(usage.line) %}
295+
{%- if link %}<a href="{{ link }}" title="{{ usage.name }}">{% else %}<span title="{{ usage.name }}">{% endif %}
296+
{{ usage.name }}
297+
{%- if link %}</a>{% else %}</span>{% endif %}
298+
<div class="text-small font-normal">
299+
{% set usage_id = 'session-usage-trace-' ~ key %}
300+
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ usage_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
301+
</div>
302+
<div id="{{ usage_id }}" class="context sf-toggle-content sf-toggle-hidden">
303+
{{ profiler_dump(usage.trace, maxDepth=2) }}
304+
</div>
305+
</td>
306+
</tr>
307+
{% endfor %}
308+
</tbody>
309+
</table>
310+
{% endif %}
253311
</div>
254312
</div>
255313

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.2.0
55
-----
66

7+
* added session usage profiling
78
* made the public `http_cache` service handle requests when available
89
* allowed enabling trusted hosts and proxies using new `kernel.trusted_hosts`,
910
`kernel.trusted_proxies` and `kernel.trusted_headers` parameters

src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
use Symfony\Component\HttpFoundation\Cookie;
1616
use Symfony\Component\HttpFoundation\ParameterBag;
1717
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\RequestStack;
1819
use Symfony\Component\HttpFoundation\Response;
20+
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
21+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1922
use Symfony\Component\HttpKernel\Event\ControllerEvent;
2023
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2124
use Symfony\Component\HttpKernel\KernelEvents;
@@ -28,10 +31,13 @@
2831
class RequestDataCollector extends DataCollector implements EventSubscriberInterface, LateDataCollectorInterface
2932
{
3033
protected $controllers;
34+
private $sessionUsages = [];
35+
private $requestStack;
3136

32-
public function __construct()
37+
public function __construct(?RequestStack $requestStack = null)
3338
{
3439
$this->controllers = new \SplObjectStorage();
40+
$this->requestStack = $requestStack;
3541
}
3642

3743
/**
@@ -105,6 +111,8 @@ public function collect(Request $request, Response $response, \Throwable $except
105111
'response_cookies' => $responseCookies,
106112
'session_metadata' => $sessionMetadata,
107113
'session_attributes' => $sessionAttributes,
114+
'session_usages' => array_values($this->sessionUsages),
115+
'stateless_check' => $this->requestStack && $this->requestStack->getMasterRequest()->attributes->get('_stateless'),
108116
'flashes' => $flashes,
109117
'path_info' => $request->getPathInfo(),
110118
'controller' => 'n/a',
@@ -175,6 +183,7 @@ public function reset()
175183
{
176184
$this->data = [];
177185
$this->controllers = new \SplObjectStorage();
186+
$this->sessionUsages = [];
178187
}
179188

180189
public function getMethod()
@@ -242,6 +251,16 @@ public function getSessionAttributes()
242251
return $this->data['session_attributes']->getValue();
243252
}
244253

254+
public function getStatelessCheck()
255+
{
256+
return $this->data['stateless_check'];
257+
}
258+
259+
public function getSessionUsages()
260+
{
261+
return $this->data['session_usages'];
262+
}
263+
245264
public function getFlashes()
246265
{
247266
return $this->data['flashes']->getValue();
@@ -382,6 +401,38 @@ public function getName()
382401
return 'request';
383402
}
384403

404+
public function collectSessionUsage(): void
405+
{
406+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
407+
408+
$traceEndIndex = \count($trace) - 1;
409+
for ($i = $traceEndIndex; $i > 0; --$i) {
410+
if (null !== ($class = $trace[$i]['class'] ?? null) && (is_subclass_of($class, SessionInterface::class) || is_subclass_of($class, SessionBagInterface::class))) {
411+
$traceEndIndex = $i;
412+
break;
413+
}
414+
}
415+
416+
if ((\count($trace) - 1) === $traceEndIndex) {
417+
return;
418+
}
419+
420+
// Remove part of the backtrace that belongs to session only
421+
array_splice($trace, 0, $traceEndIndex);
422+
423+
// Merge identical backtraces generated by internal call reports
424+
$name = sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']);
425+
if (!\array_key_exists($name, $this->sessionUsages)) {
426+
$this->sessionUsages[$name] = [
427+
'name' => $name,
428+
'file' => $trace[0]['file'],
429+
'line' => $trace[0]['line'],
430+
'trace' => $trace,
431+
'timestamp' => time(),
432+
];
433+
}
434+
}
435+
385436
/**
386437
* Parse a controller.
387438
*

src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,15 @@ public function onSessionUsage(): void
152152
return;
153153
}
154154

155-
if (!$requestStack = $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) {
156-
return;
155+
if ($dataCollector = $this->container && $this->container->has('data_collector.request') ? $this->container->get('data_collector.request') : null) {
156+
$dataCollector->collectSessionUsage();
157157
}
158158

159-
$stateless = false;
160-
$clonedRequestStack = clone $requestStack;
161-
while (null !== ($request = $clonedRequestStack->pop()) && !$stateless) {
162-
$stateless = $request->attributes->get('_stateless');
159+
if (!$requestStack = $this->container && $this->container->has('request_stack') ? $this->container->get('request_stack') : null) {
160+
return;
163161
}
164162

165-
if (!$stateless) {
163+
if (!$requestStack->getMasterRequest() || !$requestStack->getMasterRequest()->attributes->get('_stateless')) {
166164
return;
167165
}
168166

src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
use Symfony\Component\HttpFoundation\ParameterBag;
1818
use Symfony\Component\HttpFoundation\RedirectResponse;
1919
use Symfony\Component\HttpFoundation\Request;
20+
use Symfony\Component\HttpFoundation\RequestStack;
2021
use Symfony\Component\HttpFoundation\Response;
2122
use Symfony\Component\HttpFoundation\Session\Session;
23+
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
24+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
2225
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
2326
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
2427
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
@@ -248,6 +251,65 @@ public function testItCollectsTheRedirectionAndClearTheCookie()
248251
$this->assertNull($cookie->getValue());
249252
}
250253

254+
public function testItCollectsTheSessionTraceProperly(): void
255+
{
256+
$collector = new RequestDataCollector();
257+
$request = $this->createRequest();
258+
259+
// RequestDataCollectorTest doesn't implement SessionInterface or SessionBagInterface, therefore should do nothing.
260+
$collector->collectSessionUsage();
261+
262+
$collector->collect($request, $this->createResponse());
263+
$this->assertSame([], $collector->getSessionUsages());
264+
265+
$collector->reset();
266+
267+
$session = $this->createMock(SessionInterface::class);
268+
$session->method('getMetadataBag')->willReturnCallback(static function () use ($collector) {
269+
$collector->collectSessionUsage();
270+
});
271+
$session->getMetadataBag();
272+
273+
$collector->collect($request, $this->createResponse());
274+
$collector->lateCollect();
275+
276+
$usages = $collector->getSessionUsages();
277+
278+
$this->assertCount(1, $usages);
279+
$this->assertSame(__FILE__, $usages[0]['file']);
280+
$this->assertSame(__LINE__ - 9, $line = $usages[0]['line']);
281+
282+
$trace = $usages[0]['trace'];
283+
$this->assertSame('getMetadataBag', $trace[0]['function']);
284+
$this->assertSame(self::class, $class = $trace[1]['class']);
285+
286+
$this->assertSame(sprintf('%s:%s', $class, $line), $usages[0]['name']);
287+
}
288+
289+
public function testStatelessCheck(): void
290+
{
291+
$requestStack = new RequestStack();
292+
$request = $this->createRequest();
293+
$requestStack->push($this->createRequest());
294+
295+
$collector = new RequestDataCollector($requestStack);
296+
$collector->collect($request, $response = $this->createResponse());
297+
$collector->lateCollect();
298+
299+
$this->assertFalse($collector->getStatelessCheck());
300+
301+
$requestStack = new RequestStack();
302+
$request = $this->createRequest();
303+
$request->attributes->set('_stateless', true);
304+
$requestStack->push($request);
305+
306+
$collector = new RequestDataCollector($requestStack);
307+
$collector->collect($request, $response = $this->createResponse());
308+
$collector->lateCollect();
309+
310+
$this->assertTrue($collector->getStatelessCheck());
311+
}
312+
251313
protected function createRequest($routeParams = ['name' => 'foo'])
252314
{
253315
$request = Request::create('http://test.com/foo?bar=baz');

src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\HttpFoundation\Response;
2121
use Symfony\Component\HttpFoundation\Session\Session;
2222
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
23+
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
2324
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
2425
use Symfony\Component\HttpKernel\Event\RequestEvent;
2526
use Symfony\Component\HttpKernel\Event\ResponseEvent;
@@ -255,14 +256,15 @@ public function testSessionUsageCallbackWhenDebugAndStateless()
255256

256257
$request = new Request();
257258
$request->attributes->set('_stateless', true);
258-
259-
$requestStack->push(new Request());
260259
$requestStack->push($request);
261-
$requestStack->push(new Request());
260+
261+
$collector = $this->createMock(RequestDataCollector::class);
262+
$collector->expects($this->once())->method('collectSessionUsage');
262263

263264
$container = new Container();
264265
$container->set('initialized_session', $session);
265266
$container->set('request_stack', $requestStack);
267+
$container->set('data_collector.request', $collector);
266268

267269
$this->expectException(UnexpectedSessionUsageException::class);
268270
(new SessionListener($container, true))->onSessionUsage();
@@ -274,15 +276,15 @@ public function testSessionUsageCallbackWhenNoDebug()
274276
$session->method('isStarted')->willReturn(true);
275277
$session->expects($this->exactly(0))->method('save');
276278

277-
$request = new Request();
278-
$request->attributes->set('_stateless', true);
279-
280279
$requestStack = $this->getMockBuilder(RequestStack::class)->getMock();
281-
$requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request);
280+
281+
$collector = $this->createMock(RequestDataCollector::class);
282+
$collector->expects($this->never())->method('collectSessionUsage');
282283

283284
$container = new Container();
284285
$container->set('initialized_session', $session);
285286
$container->set('request_stack', $requestStack);
287+
$container->set('data_collector.request', $collector);
286288

287289
(new SessionListener($container))->onSessionUsage();
288290
}
@@ -295,7 +297,6 @@ public function testSessionUsageCallbackWhenNoStateless()
295297

296298
$requestStack = new RequestStack();
297299
$requestStack->push(new Request());
298-
$requestStack->push(new Request());
299300

300301
$container = new Container();
301302
$container->set('initialized_session', $session);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy