8000 feature #36364 [HttpKernel][WebProfilerBundle] Add session profiling … · symfony/symfony@8cc90b9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8cc90b9

Browse files
committed
feature #36364 [HttpKernel][WebProfilerBundle] Add session profiling (mtarld)
This PR was merged into the 5.2-dev branch. Discussion ---------- [HttpKernel][WebProfilerBundle] Add session profiling | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT | Doc PR | This PR proposes to add session profiling. It provides stateless checking status and session usage backtraces. Under are screesnhots of provided profiling: ![Screenshot from 2020-04-06 13-42-41](https://user-images.githubusercontent.com/4955509/78581189-d6c32580-7833-11ea-9de5-d1e4f8e60c27.png) ![Screenshot from 2020-04-06 13-43-04](https://user-images.githubusercontent.com/4955509/78581193-d88ce900-7833-11ea-90a4-85d07c64d47e.png) ![Screenshot from 2020-04-06 17-43-17](https://user-images.githubusercontent.com/4955509/78581159-cca12700-7833-11ea-98d2-38306ec9ea37.png) ![Screenshot from 2020-04-06 17-43-35](https://user-images.githubusercontent.com/4955509/78581238-e8a4c880-7833-11ea-89e2-ff4fdea8dce5.png) Commits ------- 5dbaef8 Add session profiling
2 parents 5b6139f + 5dbaef8 commit 8cc90b9

File tree

10 files changed

+198
-6
lines changed

10 files changed

+198
-6
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,16 @@
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

38+
->set('data_collector.request.session_collector', \Closure::class)
39+
->factory([\Closure::class, 'fromCallable'])
40+
->args([[service('data_collector.request'), 'collectSessionUsage']])
41+
3542
->set('data_collector.ajax', AjaxDataCollector::class)
3643
->tag('data_collector', ['template' => '@WebProfiler/Collector/ajax.html.twig', 'id' => 'ajax', 'priority' => 315])
3744

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+
'session_collector' => service('data_collector.request.session_collector')->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', 'session_collector'];
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', 'session_collector', '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
8+
49
5.0.0
510
-----
611

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

Lines changed: 54 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,54 @@
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 class="full-width">Usage</th>
282+
</tr>
283+
</thead>
284+
285+
<tbody>
286+
{% for key, usage in collector.sessionusages %}
287+
<tr>
288+
<td class="font-normal">
289+
{%- set link = usage.file|file_link(usage.line) %}
290+
{%- if link %}<a href="{{ link }}" title="{{ usage.name }}">{% else %}<span title="{{ usage.name }}">{% endif %}
291+
{{ usage.name }}
292+
{%- if link %}</a>{% else %}</span>{% endif %}
293+
<div class="text-small font-normal">
294+
{% set usage_id = 'session-usage-trace-' ~ key %}
295+
<a class="btn btn-link text-small sf-toggle" data-toggle-selector="#{{ usage_id }}" data-toggle-alt-content="Hide trace">Show trace</a>
296+
</div>
297+
<div id="{{ usage_id }}" class="context sf-toggle-content sf-toggle-hidden">
298+
{{ profiler_dump(usage.trace, maxDepth=2) }}
299+
</div>
300+
</td>
301+
</tr>
302+
{% endfor %}
303+
</tbody>
304+
</table>
305+
{% endif %}
253306
</div>
254307
</div>
255308

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
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: 51 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', false),
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,37 @@ 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 t 10000 hat 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+
];
432+
}
433+
}
434+
385435
/**
386436
* Parse a controller.
387437
*

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 ($this->container && $this->container->has('session_collector')) {
156+
$this->container->get('session_collector')();
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: 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()
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()
290+
{
291+
$requestStack = new RequestStack();
292+
$request = $this->createRequest();
293+
$requestStack->push($request);
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: 11 additions & 2 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('session_collector', \Closure::fromCallable([$collector, 'collectSessionUsage']));
266271

267272
$this->expectException(UnexpectedSessionUsageException::class);
268273
(new SessionListener($container, true))->onSessionUsage();
@@ -277,12 +282,16 @@ public function testSessionUsageCallbackWhenNoDebug()
277282
$request = new Request();
278283
$request->attributes->set('_stateless', true);
279284

280-
$requestStack = $this->getMockBuilder(RequestStack::class)->getMock();
281-
$requestStack->expects($this->never())->method('getMasterRequest')->willReturn($request);
285+
$requestStack = new RequestStack();
286+
$requestStack->push($request);
287+
288+
$collector = $this->createMock(RequestDataCollector::class);
289+
$collector->expects($this->never())->method('collectSessionUsage');
282290

283291
$container = new Container();
284292
$container->set('initialized_session', $session);
285293
$container->set('request_stack', $requestStack);
294+
$container->set('session_collector', $collector);
286295

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

0 commit comments

Comments
 (0)
0