Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 649e4ef

Browse files
committed
[Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands
1 parent c6930e3 commit 649e4ef

File tree

33 files changed

+1603
-149
lines changed

33 files changed

+1603
-149
lines changed

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

Lines 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

Lines 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

Lines 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
}
Lines 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 <[email protected]>
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 <[email protected]>
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

Lines 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

Lines 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 <[email protected]>
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 <[email protected]>
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

Lines 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)