Skip to content

Commit afade8a

Browse files
committed
Add session profiling
1 parent a184e54 commit afade8a

File tree

10 files changed

+219
-4
lines changed

10 files changed

+219
-4
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>No session usage were made.</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: 60 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
/**
@@ -86,6 +92,14 @@ public function collect(Request $request, Response $response, \Throwable $except
8692
}
8793
}
8894

95+
$requestStateless = false;
96+
if ($this->requestStack) {
97+
$clonedRequestStack = clone $this->requestStack;
98+
while (null !== ($clonedRequest = $clonedRequestStack->pop()) && !$requestStateless) {
99+
$requestStateless = $clonedRequest->attributes->get('_stateless');
100+
}
101+
}
102+
89103
$this->data = [
90104
'method' => $request->getMethod(),
91105
'format' => $request->getRequestFormat(),
@@ -105,6 +119,8 @@ public function collect(Request $request, Response $response, \Throwable $except
105119
'response_cookies' => $responseCookies,
106120
'session_metadata' => $sessionMetadata,
107121
'session_attributes' => $sessionAttributes,
122+
'session_usages' => array_values($this->sessionUsages),
123+
'stateless_check' => $requestStateless ?: false,
108124
'flashes' => $flashes,
109125
'path_info' => $request->getPathInfo(),
110126
'controller' => 'n/a',
@@ -175,6 +191,7 @@ public function reset()
175191
{
176192
$this->data = [];
177193
$this->controllers = new \SplObjectStorage();
194+
$this->sessionUsages = [];
178195
}
179196

180197
public function getMethod()
@@ -242,6 +259,16 @@ public function getSessionAttributes()
242259
return $this->data['session_attributes']->getValue();
243260
}
244261

262+
public function getStatelessCheck()
263+
{
264+
return $this->data['stateless_check'];
265+
}
266+
267+
public function getSessionUsages()
268+
{
269+
return $this->data['session_usages'];
270+
}
271+
245272
public function getFlashes()
246273
{
247274
return $this->data['flashes']->getValue();
@@ -382,6 +409,38 @@ public function getName()
382409
return 'request';
383410
}
384411

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

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ public function onSessionUsage(): void
152152
return;
153153
}
154154

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

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

Lines changed: 75 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,78 @@ 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+
$requestStack = new RequestStack();
313+
$masterRequest = $this->createRequest();
314+
$subRequest = $this->createRequest();
315+
$subRequest->attributes->set('_stateless', true);
316+
$requestStack->push($subRequest);
317+
$requestStack->push($masterRequest);
318+
319+
$collector = new RequestDataCollector($requestStack);
320+
$collector->collect($request, $response = $this->createResponse());
321+
$collector->lateCollect();
322+
323+
$this->assertTrue($collector->getStatelessCheck());
324+
}
325+
251326
protected function createRequest($routeParams = ['name' => 'foo'])
252327
{
253328
$request = Request::create('http://test.com/foo?bar=baz');

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

Lines changed: 9 additions & 0 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;
@@ -260,9 +261,13 @@ public function testSessionUsageCallbackWhenDebugAndStateless()
260261
$requestStack->push($request);
261262
$requestStack->push(new Request());
262263

264+
$collector = $this->createMock(RequestDataCollector::class);
265+
$collector->expects($this->once())->method('collectSessionUsage');
266+
263267
$container = new Container();
264268
$container->set('initialized_session', $session);
265269
$container->set('request_stack', $requestStack);
270+
$container->set('data_collector.request', $collector);
266271

267272
$this->expectException(UnexpectedSessionUsageException::class);
268273
(new SessionListener($container, true))->onSessionUsage();
@@ -280,9 +285,13 @@ public function testSessionUsageCallbackWhenNoDebug()
280285
$requestStack = $this->getMockBuilder(RequestStack::class)->getMock();
281286
$requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request);
282287

288+
$collector = $this->createMock(RequestDataCollector::class);
289+
$collector->expects($this->never())->method('collectSessionUsage');
290+
283291
$container = new Container();
284292
$container->set('initialized_session', $session);
285293
$container->set('request_stack', $requestStack);
294+
$container->set('data_collector.request', $collector);
286295

287296
(new SessionListener($container))->onSessionUsage();
288297
}

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