8000 Add session profiling · symfony/symfony@503b6ad · GitHub
[go: up one dir, main page]

Skip to content

Commit 503b6ad

Browse files
committed
Add session profiling
1 parent a184e54 commit 503b6ad

File tree

10 files changed

+220
-4
lines changed

10 files changed

+220
-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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111

1212
namespace Symfony\Component\HttpKernel\Tests\DataCollector;
1313

14+
use phpDocumentor\Reflection\Types\This;
1415
use PHPUnit\Framework\TestCase;
1516
use Symfony\Component\EventDispatcher\EventDispatcher;
1617
use Symfony\Component\HttpFoundation\Cookie;
1718
use Symfony\Component\HttpFoundation\ParameterBag;
1819
use Symfony\Component\HttpFoundation\RedirectResponse;
1920
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\HttpFoundation\RequestStack;
2022
use Symfony\Component\HttpFoundation\Response;
2123
use Symfony\Component\HttpFoundation\Session\Session;
24+
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
25+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
2226
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
2327
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
2428
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector;
@@ -248,6 +252,78 @@ public function testItCollectsTheRedirectionAndClearTheCookie()
248252
$this->assertNull($cookie->getValue());
249253
}
250254

255+
public function testItCollectsTheSessionTraceProperly(): void
256+
{
257+
$collector = new RequestDataCollector();
258+
$request = $this->createRequest();
259+
260+
// RequestDataCollectorTest doesn't implement SessionInterface or SessionBagInterface, therefore should do nothing.
261+
$collector->collectSessionUsage();
262+
263+
$collector->collect($request, $this->createResponse());
264+
$this->assertSame([], $collector->getSessionUsages());
265+
266+
$collector->reset();
267+
268+
$session = $this->createMock(SessionInterface::class);
269+
$session->method('getMetadataBag')->willReturnCallback(static function () use ($collector) {
270+
$collector->collectSessionUsage();
271+
});
272+
$session->getMetadataBag();
273+
274+
$collector->collect($request, $this->createResponse());
275+
$collector->lateCollect();
276+
277+
$usages = $collector->getSessionUsages();
278+
279+
$this->assertCount(1, $usages);
280+
$this->assertSame(__FILE__, $usages[0]['file']);
281+
$this->assertSame(__LINE__ - 9, $line = $usages[0]['line']);
282+
283+
$trace = $usages[0]['trace'];
284+
$this->assertSame('getMetadataBag', $trace[0]['function']);
285+
$this->assertSame(self::class, $class = $trace[1]['class']);
286+
287+
$this->assertSame(sprintf('%s:%s', $class, $line), $usages[0]['name']);
288+
}
289+
290+
public function testStatelessCheck(): void
291+
{
292+
$requestStack = new RequestStack();
293+
$request = $this->createRequest();
294+
$requestStack->push($this->createRequest());
295+
296+
$collector = new RequestDataCollector($requestStack);
297+
$collector->collect($request, $response = $this->createResponse());
298+
$collector->lateCollect();
299+
300+
$this->assertFalse($collector->getStatelessCheck());
301+
302+
$requestStack = new RequestStack();
303+
$request = $this->createRequest();
304+
$request->attributes->set('_stateless', true);
305+
$requestStack->push($request);
306+
307+
$collector = new RequestDataCollector($requestStack);
308+
$collector->collect($request, $response = $this->createResponse());
309+
$collector->lateCollect();
310+
311+
$this->assertTrue($collector->getStatelessCheck());
312+
313+
$requestStack = new RequestStack();
314+
$masterRequest = $this->createRequest();
315+
$subRequest = $this->createRequest();
316+
$subRequest->attributes->set('_stateless', true);
317+
$requestStack->push($subRequest);
318+
$requestStack->push($masterRequest);
319+
320+
$collector = new RequestDataCollector($requestStack);
321+
$collector->collect($request, $response = $this->createResponse());
322+
$collector->lateCollect();
323+
324+
$this->assertTrue($collector->getStatelessCheck());
325+
}
326+
251327
protected function createRequest($routeParams = ['name' => 'foo'])
252328
{
253329
$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)
0