Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 649e4ef

Browse filesBrowse files
committed
[Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands
1 parent c6930e3 commit 649e4ef
Copy full SHA for 649e4ef

33 files changed

+1603
-149
lines changed

‎src/Symfony/Bundle/DebugBundle/Resources/config/services.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/DebugBundle/Resources/config/services.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
service('debug.stopwatch')->ignoreOnInvalid(),
5151
service('debug.file_link_formatter')->ignoreOnInvalid(),
5252
param('kernel.charset'),
53-
service('request_stack'),
53+
service('.virtual_request_stack'),
5454
null, // var_dumper.cli_dumper or var_dumper.server_connection when debug.dump_destination is set
5555
])
5656
->tag('data_collector', [

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ CHANGELOG
3333
* Add parameters deprecations to the output of `debug:container` command
3434
* Change `framework.asset_mapper.importmap_polyfill` from a URL to the name of an item in the importmap
3535
* Provide `$buildDir` when running `CacheWarmer` to build read-only resources
36+
* Add the global `--profile` option to the console to enable profiling commands
3637

3738
6.3
3839
---

‎src/Symfony/Bundle/FrameworkBundle/Console/Application.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Console/Application.php
+35-3Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Symfony\Component\Console\Application as BaseApplication;
1515
use Symfony\Component\Console\Command\Command;
1616
use Symfony\Component\Console\Command\ListCommand;
17+
use Symfony\Component\Console\Command\TraceableCommand;
18+
use Symfony\Component\Console\Debug\CliRequest;
1719
use Symfony\Component\Console\Input\InputInterface;
1820
use Symfony\Component\Console\Input\InputOption;
1921
use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -42,6 +44,7 @@ public function __construct(KernelInterface $kernel)
4244
$inputDefinition = $this->getDefinition();
4345
$inputDefinition->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $kernel->getEnvironment()));
4446
$inputDefinition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switch off debug mode.'));
47+
$inputDefinition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Enables profiling (requires debug).'));
4548
}
4649

4750
/**
@@ -79,18 +82,47 @@ public function doRun(InputInterface $input, OutputInterface $output): int
7982

8083
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
8184
{
85+
$requestStack = null;
86+
$renderRegistrationErrors = true;
87+
8288
if (!$command instanceof ListCommand) {
8389
if ($this->registrationErrors) {
8490
$this->renderRegistrationErrors($input, $output);
8591
$this->registrationErrors = [];
92+
$renderRegistrationErrors = false;
8693
}
94+
}
95+
96+
if ($input->hasParameterOption('--profile')) {
97+
$container = $this->kernel->getContainer();
8798

88-
return parent::doRunCommand($command, $input, $output);
99+
if (!$this->kernel->isDebug()) {
100+
if ($output instanceof ConsoleOutputInterface) {
101+
$output = $output->getErrorOutput();
102+
}
103+
104+
(new SymfonyStyle($input, $output))->warning('Debug mode should be enabled when the "--profile" option is used.');
105+
} elseif (!$container->has('debug.stopwatch')) {
106+
if ($output instanceof ConsoleOutputInterface) {
107+
$output = $output->getErrorOutput();
108+
}
109+
110+
(new SymfonyStyle($input, $output))->warning('The "--profile" option needs the Stopwatch component. Try running "composer require symfony/stopwatch".');
111+
} else {
112+
$command = new TraceableCommand($command, $container->get('debug.stopwatch'));
113+
114+
$requestStack = $container->get('.virtual_request_stack');
115+
$requestStack->push(new CliRequest($command));
116+
}
89117
}
90118

91-
$returnCode = parent::doRunCommand($command, $input, $output);
119+
try {
120+
$returnCode = parent::doRunCommand($command, $input, $output);
121+
} finally {
122+
$requestStack?->pop();
123+
}
92124

93-
if ($this->registrationErrors) {
125+
if ($renderRegistrationErrors && $this->registrationErrors) {
94126
$this->renderRegistrationErrors($input, $output);
95127
$this->registrationErrors = [];
96128
}
+58Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Debug;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Console\Debug\CliRequest;
16+
use Symfony\Component\Console\Event\ConsoleCommandEvent;
17+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18+
use Symfony\Component\HttpFoundation\RequestStack;
19+
use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher as BaseTraceableEventDispatcher;
20+
use Symfony\Component\Stopwatch\Stopwatch;
21+
22+
/**
23+
* @internal
24+
*
25+
* @author Jules Pietri <jules@heahprod.com>
26+
*/
27+
final class TraceableEventDispatcher extends BaseTraceableEventDispatcher
28+
{
29+
public function __construct(
30+
EventDispatcherInterface $dispatcher,
31+
Stopwatch $stopwatch,
32+
LoggerInterface $logger = null,
33+
private readonly ?RequestStack $requestStack = null
34+
) {
35+
parent::__construct($dispatcher, $stopwatch, $logger, $requestStack);
36+
}
37+
38+
/**
39+
* Starts a stopwatch section before a command runs to profile listeners.
40+
*/
41+
protected function beforeDispatch(string $eventName, object $event): void
42+
{
43+
if (!$event instanceof ConsoleCommandEvent) {
44+
parent::beforeDispatch($eventName, $event);
45+
46+
return;
47+
}
48+
49+
$request = $this->requestStack->getCurrentRequest();
50+
51+
if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) {
52+
return;
53+
}
54+
55+
$request->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
56+
$this->stopwatch->openSection();
57+
}
58+
}

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+9-3Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Symfony\Component\Config\ResourceCheckerInterface;
5151
use Symfony\Component\Console\Application;
5252
use Symfony\Component\Console\Command\Command;
53+
use Symfony\Component\Console\Debug\CliRequest;
5354
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
5455
use Symfony\Component\DependencyInjection\Alias;
5556
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@@ -912,6 +913,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
912913

913914
$container->getDefinition('profiler_listener')
914915
->addArgument($config['collect_parameter']);
916+
917+
if (!$container->getParameter('kernel.debug') || !class_exists(CliRequest::class) || !$container->has('debug.stopwatch')) {
918+
$container->removeDefinition('console_profiler_listener');
919+
}
915920
}
916921

917922
private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
@@ -1134,15 +1139,16 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con
11341139
{
11351140
$loader->load('debug_prod.php');
11361141

1142+
$debug = $container->getParameter('kernel.debug');
1143+
11371144
if (class_exists(Stopwatch::class)) {
11381145
$container->register('debug.stopwatch', Stopwatch::class)
11391146
->addArgument(true)
1147+
->setPublic($debug)
11401148
->addTag('kernel.reset', ['method' => 'reset']);
11411149
$container->setAlias(Stopwatch::class, new Alias('debug.stopwatch', false));
11421150
}
11431151

1144-
$debug = $container->getParameter('kernel.debug');
1145-
11461152
if ($debug && !$container->hasParameter('debug.container.dump')) {
11471153
$container->setParameter('debug.container.dump', '%kernel.build_dir%/%kernel.container_class%.xml');
11481154
}
@@ -1165,7 +1171,7 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con
11651171

11661172
if ($debug && class_exists(DebugProcessor::class)) {
11671173
$definition = new Definition(DebugProcessor::class);
1168-
$definition->addArgument(new Reference('request_stack'));
1174+
$definition->addArgument(new Reference('.virtual_request_stack'));
11691175
$definition->addTag('kernel.reset', ['method' => 'reset']);
11701176
$container->setDefinition('debug.log_processor', $definition);
11711177

+133Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\EventListener;
13+
14+
use Symfony\Component\Console\ConsoleEvents;
15+
use Symfony\Component\Console\Debug\CliRequest;
16+
use Symfony\Component\Console\Event\ConsoleCommandEvent;
17+
use Symfony\Component\Console\Event\ConsoleErrorEvent;
18+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
19+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
20+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
21+
use Symfony\Component\HttpFoundation\Request;
22+
use Symfony\Component\HttpFoundation\RequestStack;
23+
use Symfony\Component\HttpKernel\Profiler\Profile;
24+
use Symfony\Component\HttpKernel\Profiler\Profiler;
25+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
26+
use Symfony\Component\Stopwatch\Stopwatch;
27+
28+
/**
29+
* @internal
30+
*
31+
* @author Jules Pietri <jules@heahprod.com>
32+
*/
33+
final class ConsoleProfilerListener implements EventSubscriberInterface
34+
{
35+
private ?\Throwable $error = null;
36+
/** @var \SplObjectStorage<Request, Profile> */
37+
private \SplObjectStorage $profiles;
38+
/** @var \SplObjectStorage<Request, ?Request> */
39+
private \SplObjectStorage $parents;
40+
41+
public function __construct(
42+
private readonly Profiler $profiler,
43+
private readonly RequestStack $requestStack,
44+
private readonly Stopwatch $stopwatch,
45+
private readonly UrlGeneratorInterface $urlGenerator,
46+
) {
47+
$this->profiles = new \SplObjectStorage();
48+
$this->parents = new \SplObjectStorage();
49+
}
50+
51+
public static function getSubscribedEvents(): array
52+
{
53+
return [
54+
ConsoleEvents::COMMAND => ['initialize', 2048],
55+
ConsoleEvents::ERROR => ['catch', -2048],
56+
ConsoleEvents::TERMINATE => ['profile', -2048],
57+
];
58+
}
59+
60+
public function initialize(ConsoleCommandEvent $event): void
61+
{
62+
if (!$event->getInput()->getOption('profile')) {
63+
$this->profiler->disable();
64+
}
65+
}
66+
67+
public function catch(ConsoleErrorEvent $event): void
68+
{
69+
$this->error = $event->getError();
70+
}
71+
72+
public function profile(ConsoleTerminateEvent $event): void
73+
{
74+
if (!$this->profiler->isEnabled()) {
75+
return;
76+
}
77+
78+
$request = $this->requestStack->getCurrentRequest();
79+
80+
if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) {
81+
return;
82+
}
83+
84+
if (null !== $sectionId = $request->attributes->get('_stopwatch_token')) {
85+
// we must close the section before saving the profile to allow late collect
86+
try {
87+
$this->stopwatch->stopSection($sectionId);
88+
} catch (\LogicException) {
89+
// noop
90+
}
91+
}
92+
93+
$request->command->exitCode = $event->getExitCode();
94+
$request->command->interruptedBySignal = $event->getInterruptingSignal();
95+
96+
$profile = $this->profiler->collect($request, $request->getResponse(), $this->error);
97+
$this->error = null;
98+
$this->profiles[$request] = $profile;
99+
100+
if ($this->parents[$request] = $this->requestStack->getParentRequest()) {
101+
// do not save on sub commands
102+
return;
103+
}
104+
105+
// attach children to parents
106+
foreach ($this->profiles as $request) {
107+
if (null !== $parentRequest = $this->parents[$request]) {
108+
if (isset($this->profiles[$parentRequest])) {
109+
$this->profiles[$parentRequest]->addChild($this->profiles[$request]);
110+
}
111+
}
112+
}
113+
114+
$output = $event->getOutput();
115+
$output = $output instanceof ConsoleOutputInterface && $output->isVerbose() ? $output->getErrorOutput() : null;
116+
117+
// save profiles
118+
foreach ($this->profiles as $r) {
119+
$p = $this->profiles[$r];
120+
$this->profiler->saveProfile($p);
121+
122+
$token = $p->getToken();
123+
$output?->writeln(sprintf(
124+
'See profile <href=%s>%s</>',
125+
$this->urlGenerator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL),
126+
$token
127+
));
128+
}
129+
130+
$this->profiles = new \SplObjectStorage();
131+
$this->parents = new \SplObjectStorage();
132+
}
133+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php
+7-3Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

1414
use Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector;
15+
use Symfony\Component\Console\DataCollector\CommandDataCollector;
1516
use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector;
1617
use Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector;
1718
use Symfony\Component\HttpKernel\DataCollector\EventDataCollector;
@@ -30,7 +31,7 @@
3031

3132
->set('data_collector.request', RequestDataCollector::class)
3233
->args([
33-
service('request_stack')->ignoreOnInvalid(),
34+
service('.virtual_request_stack')->ignoreOnInvalid(),
3435
])
3536
->tag('kernel.event_subscriber')
3637
->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335])
@@ -48,15 +49,15 @@
4849
->set('data_collector.events', EventDataCollector::class)
4950
->args([
5051
tagged_iterator('event_dispatcher.dispatcher', 'name'),
51-
service('request_stack')->ignoreOnInvalid(),
52+
service('.virtual_request_stack')->ignoreOnInvalid(),
5253
])
5354
->tag('data_collector', ['template' => '@WebProfiler/Collector/events.html.twig', 'id' => 'events', 'priority' => 290])
5455

5556
->set('data_collector.logger', LoggerDataCollector::class)
5657
->args([
5758
service('logger')->ignoreOnInvalid(),
5859
sprintf('%s/%s', param('kernel.build_dir'), param('kernel.container_class')),
59-
service('request_stack')->ignoreOnInvalid(),
60+
service('.virtual_request_stack')->ignoreOnInvalid(),
6061
])
6162
->tag('monolog.logger', ['channel' => 'profiler'])
6263
->tag('data_collector', ['template' => '@WebProfiler/Collector/logger.html.twig', 'id' => 'logger', 'priority' => 300])
@@ -74,5 +75,8 @@
7475
->set('data_collector.router', RouterDataCollector::class)
7576
->tag('kernel.event_listener', ['event' => KernelEvents::CONTROLLER, 'method' => 'onKernelController'])
7677
->tag('data_collector', ['template' => '@WebProfiler/Collector/router.html.twig', 'id' => 'router', 'priority' => 285])
78+
79+
->set('.data_collector.command', CommandDataCollector::class)
80+
->tag('data_collector', ['template' => '@WebProfiler/Collector/command.html.twig', 'id' => 'command', 'priority' => 335])
7781
;
7882
};

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.