Skip to content

Commit e495a82

Browse files
committed
Add session profiling
1 parent 1fc7b86 commit e495a82

File tree

8 files changed

+178
-5
lines changed

8 files changed

+178
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
<argument key="session" type="service" id="session" on-invalid="ignore" />
7878
<argument key="initialized_session" type="service" id="session" on-invalid="ignore_uninitialized" />
7979
<argument key="logger" type="service" id="logger" on-invalid="ignore" />
80+
<argument key="data_collector.request" type="service" id="data_collector.request" on-invalid="ignore" />
8081
</argument>
8182
<argument>%kernel.debug%</argument>
8283
</service>

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.1.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
@@ -8,6 +8,7 @@ CHANGELOG
88
* allowed using public aliases to reference controllers
99
* added session usage reporting when the `_stateless` attribute of the request is set to `true`
1010
* added `AbstractSessionListener::onSessionUsage()` to report when the session is used while a request is stateless
11+
* added session usage profiling
1112

1213
5.0.0
1314
-----

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Symfony\Component\HttpFoundation\ParameterBag;
1717
use Symfony\Component\HttpFoundation\Request;
1818
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
20+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1921
use Symfony\Component\HttpKernel\Event\ControllerEvent;
2022
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2123
use Symfony\Component\HttpKernel\KernelEvents;
@@ -28,6 +30,7 @@
2830
class RequestDataCollector extends DataCollector implements EventSubscriberInterface, LateDataCollectorInterface
2931
{
3032
protected $controllers;
33+
protected $sessionUsages = [];
3134

3235
public function __construct()
3336
{
@@ -105,6 +108,8 @@ public function collect(Request $request, Response $response, \Throwable $except
105108
'response_cookies' => $responseCookies,
106109
'session_metadata' => $sessionMetadata,
107110
'session_attributes' => $sessionAttributes,
111+
'session_usages' => $this->sessionUsages,
112+
'stateless_check' => $request->attributes->get('_stateless'),
108113
'flashes' => $flashes,
109114
'path_info' => $request->getPathInfo(),
110115
'controller' => 'n/a',
@@ -175,6 +180,7 @@ public function reset()
175180
{
176181
$this->data = [];
177182
$this->controllers = new \SplObjectStorage();
183+
$this->sessionUsages = [];
178184
}
179185

180186
public function getMethod()
@@ -242,6 +248,16 @@ public function getSessionAttributes()
242248
return $this->data['session_attributes']->getValue();
243249
}
244250

251+
public function getStatelessCheck()
252+
{
253+
return $this->data['stateless_check'];
254+
}
255+
256+
public function getSessionUsages()
257+
{
258+
return $this->data['session_usages'];
259+
}
260+
245261
public function getFlashes()
246262
{
247263
return $this->data['flashes']->getValue();
@@ -382,6 +398,34 @@ public function getName()
382398
return 'request';
383399
}
384400

401+
public function collectSessionUsage(): void
402+
{
403+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
404+
405+
$traceEndIndex = \count($trace) - 1;
406+
for ($i = $traceEndIndex; $i > 0; --$i) {
407+
if (null !== ($class = $trace[$i]['class'] ?? null) && (is_subclass_of($class, SessionInterface::class) || is_subclass_of($class, SessionBagInterface::class))) {
408+
$traceEndIndex = $i;
409+
break;
410+
}
411+
}
412+
413+
if ((\count($trace) - 1) === $traceEndIndex) {
414+
return;
415+
}
416+
417+
// Remove part of the backtrace that belongs to session only
418+
\array_splice($trace, 0, $traceEndIndex);
419+
420+
$this->sessionUsages[] = [
421+
'name' => sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']),
422+
'file' => $trace[0]['file'],
423+
'line' => $trace[0]['line'],
424+
'trace' => $trace,
425+
'timestamp' => time(),
426+
];
427+
}
428+
385429
/**
386430
* Parse a controller.
387431
*

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ public function onSessionUsage(): void
156156
return;
157157
}
158158

159+
if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $requestStack->getCurrentRequest()->getSession()) {
160+
return;
161+
}
162+
163+
if ($dataCollector = $this->container && $this->container->has('data_collector.request') ? $this->container->get('data_collector.request') : null) {
164+
$dataCollector->collectSessionUsage();
165+
}
166+
159167
$stateless = false;
160168
$clonedRequestStack = clone $requestStack;
161169
while (null !== ($request = $clonedRequestStack->pop()) && !$stateless) {
@@ -166,10 +174,6 @@ public function onSessionUsage(): void
166174
return;
167175
}
168176

169-
if (!$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : $requestStack->getCurrentRequest()->getSession()) {
170-
return;
171-
}
172-
173177
if ($session->isStarted()) {
174178
$session->save();
175179
}

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
use Symfony\Component\HttpFoundation\Request;
2020
use Symfony\Component\HttpFoundation\Response;
2121
use Symfony\Component\HttpFoundation\Session\Session;
22+
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
23+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
24+
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
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,54 @@ 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+
$collector = new RequestDataCollector();
292+
293+
$request = $this->createRequest();
294+
$request->attributes->set('_stateless', true);
295+
296+
$collector->collect($request, $response = $this->createResponse());
297+
$collector->lateCollect();
298+
299+
$this->assertTrue($collector->getStatelessCheck());
300+
}
301+
251302
protected function createRequest($routeParams = ['name' => 'foo'])
252303
{
253304
$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