From 82914bab0df67d0b1b4920d7a576dd850b5c0be1 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Mon, 15 Aug 2022 10:48:40 +0200 Subject: [PATCH] [Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands --- .../DebugBundle/Resources/config/services.php | 2 +- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../FrameworkBundle/Console/Application.php | 38 +- .../FrameworkExtension.php | 12 +- .../EventListener/ConsoleProfilerListener.php | 144 +++++++ .../Resources/config/collectors.php | 10 +- .../Resources/config/debug.php | 7 +- .../Resources/config/profiling.php | 10 + .../Tests/Console/ApplicationTest.php | 17 +- .../Bundle/WebProfilerBundle/CHANGELOG.md | 5 + .../Controller/ProfilerController.php | 20 +- .../views/Collector/command.html.twig | 249 ++++++++++++ .../Resources/views/Collector/time.html.twig | 14 +- .../Resources/views/Icon/command.svg | 6 + .../views/Profiler/_command_summary.html.twig | 44 +++ .../views/Profiler/_request_summary.html.twig | 99 +++++ .../Resources/views/Profiler/header.html.twig | 15 + .../Resources/views/Profiler/layout.html.twig | 123 +----- .../views/Profiler/results.html.twig | 38 +- .../Resources/views/Profiler/search.html.twig | 45 ++- .../Controller/ProfilerControllerTest.php | 3 + .../Bundle/WebProfilerBundle/composer.json | 2 +- .../Console/Command/TraceableCommand.php | 356 ++++++++++++++++++ .../DataCollector/CommandDataCollector.php | 234 ++++++++++++ .../Component/Console/Debug/CliRequest.php | 70 ++++ src/Symfony/Component/Console/composer.json | 3 + src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../DataCollector/EventDataCollector.php | 1 - .../HttpKernel/Debug/VirtualRequestStack.php | 65 ++++ .../Profiler/FileProfilerStorage.php | 24 +- .../Component/HttpKernel/Profiler/Profile.php | 19 +- .../HttpKernel/Profiler/Profiler.php | 17 +- .../Profiler/ProfilerStorageInterface.php | 9 +- .../Profiler/FileProfilerStorageTest.php | 26 ++ 34 files changed, 1570 insertions(+), 159 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/command.html.twig create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/command.svg create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_command_summary.html.twig create mode 100644 src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_request_summary.html.twig create mode 100644 src/Symfony/Component/Console/Command/TraceableCommand.php create mode 100644 src/Symfony/Component/Console/DataCollector/CommandDataCollector.php create mode 100644 src/Symfony/Component/Console/Debug/CliRequest.php create mode 100644 src/Symfony/Component/HttpKernel/Debug/VirtualRequestStack.php diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.php b/src/Symfony/Bundle/DebugBundle/Resources/config/services.php index d0f57c092872e..ea2d057310f88 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.php @@ -50,7 +50,7 @@ service('debug.stopwatch')->ignoreOnInvalid(), service('debug.file_link_formatter')->ignoreOnInvalid(), param('kernel.charset'), - service('request_stack'), + service('.virtual_request_stack'), null, // var_dumper.cli_dumper or var_dumper.server_connection when debug.dump_destination is set ]) ->tag('data_collector', [ diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 086447f717fe2..35e57b822c38c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -33,6 +33,7 @@ CHANGELOG * Add parameters deprecations to the output of `debug:container` command * Change `framework.asset_mapper.importmap_polyfill` from a URL to the name of an item in the importmap * Provide `$buildDir` when running `CacheWarmer` to build read-only resources + * Add the global `--profile` option to the console to enable profiling commands 6.3 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 80ecf288c519a..1fe1e57feb1be 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -14,6 +14,8 @@ use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\Command\TraceableCommand; +use Symfony\Component\Console\Debug\CliRequest; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; @@ -42,6 +44,7 @@ public function __construct(KernelInterface $kernel) $inputDefinition = $this->getDefinition(); $inputDefinition->addOption(new InputOption('--env', '-e', InputOption::VALUE_REQUIRED, 'The Environment name.', $kernel->getEnvironment())); $inputDefinition->addOption(new InputOption('--no-debug', null, InputOption::VALUE_NONE, 'Switch off debug mode.')); + $inputDefinition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Enables profiling (requires debug).')); } /** @@ -79,18 +82,47 @@ public function doRun(InputInterface $input, OutputInterface $output): int protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int { + $requestStack = null; + $renderRegistrationErrors = true; + if (!$command instanceof ListCommand) { if ($this->registrationErrors) { $this->renderRegistrationErrors($input, $output); $this->registrationErrors = []; + $renderRegistrationErrors = false; } + } + + if ($input->hasParameterOption('--profile')) { + $container = $this->kernel->getContainer(); - return parent::doRunCommand($command, $input, $output); + if (!$this->kernel->isDebug()) { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + (new SymfonyStyle($input, $output))->warning('Debug mode should be enabled when the "--profile" option is used.'); + } elseif (!$container->has('debug.stopwatch')) { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + (new SymfonyStyle($input, $output))->warning('The "--profile" option needs the Stopwatch component. Try running "composer require symfony/stopwatch".'); + } else { + $command = new TraceableCommand($command, $container->get('debug.stopwatch')); + + $requestStack = $container->get('.virtual_request_stack'); + $requestStack->push(new CliRequest($command)); + } } - $returnCode = parent::doRunCommand($command, $input, $output); + try { + $returnCode = parent::doRunCommand($command, $input, $output); + } finally { + $requestStack?->pop(); + } - if ($this->registrationErrors) { + if ($renderRegistrationErrors && $this->registrationErrors) { $this->renderRegistrationErrors($input, $output); $this->registrationErrors = []; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index bd258c850fc01..c48c8c9eea128 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -50,6 +50,7 @@ use Symfony\Component\Config\ResourceCheckerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Debug\CliRequest; use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; @@ -912,6 +913,10 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $container->getDefinition('profiler_listener') ->addArgument($config['collect_parameter']); + + if (!$container->getParameter('kernel.debug') || !class_exists(CliRequest::class) || !$container->has('debug.stopwatch')) { + $container->removeDefinition('console_profiler_listener'); + } } private function registerWorkflowConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void @@ -1134,15 +1139,16 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con { $loader->load('debug_prod.php'); + $debug = $container->getParameter('kernel.debug'); + if (class_exists(Stopwatch::class)) { $container->register('debug.stopwatch', Stopwatch::class) ->addArgument(true) + ->setPublic($debug) ->addTag('kernel.reset', ['method' => 'reset']); $container->setAlias(Stopwatch::class, new Alias('debug.stopwatch', false)); } - $debug = $container->getParameter('kernel.debug'); - if ($debug && !$container->hasParameter('debug.container.dump')) { $container->setParameter('debug.container.dump', '%kernel.build_dir%/%kernel.container_class%.xml'); } @@ -1165,7 +1171,7 @@ private function registerDebugConfiguration(array $config, ContainerBuilder $con if ($debug && class_exists(DebugProcessor::class)) { $definition = new Definition(DebugProcessor::class); - $definition->addArgument(new Reference('request_stack')); + $definition->addArgument(new Reference('.virtual_request_stack')); $definition->addTag('kernel.reset', ['method' => 'reset']); $container->setDefinition('debug.log_processor', $definition); diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php new file mode 100644 index 0000000000000..f9a55a62e23b9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/ConsoleProfilerListener.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\EventListener; + +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Debug\CliRequest; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @internal + * + * @author Jules Pietri + */ +final class ConsoleProfilerListener implements EventSubscriberInterface +{ + private ?\Throwable $error = null; + /** @var \SplObjectStorage */ + private \SplObjectStorage $profiles; + /** @var \SplObjectStorage */ + private \SplObjectStorage $parents; + + public function __construct( + private readonly Profiler $profiler, + private readonly RequestStack $requestStack, + private readonly Stopwatch $stopwatch, + private readonly UrlGeneratorInterface $urlGenerator, + ) { + $this->profiles = new \SplObjectStorage(); + $this->parents = new \SplObjectStorage(); + } + + public static function getSubscribedEvents(): array + { + return [ + ConsoleEvents::COMMAND => ['initialize', 4096], + ConsoleEvents::ERROR => ['catch', -2048], + ConsoleEvents::TERMINATE => ['profile', -4096], + ]; + } + + public function initialize(ConsoleCommandEvent $event): void + { + if (!$event->getInput()->getOption('profile')) { + $this->profiler->disable(); + + return; + } + + $request = $this->requestStack->getCurrentRequest(); + + if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) { + return; + } + + $request->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); + $this->stopwatch->openSection(); + } + + public function catch(ConsoleErrorEvent $event): void + { + $this->error = $event->getError(); + } + + public function profile(ConsoleTerminateEvent $event): void + { + if (!$this->profiler->isEnabled()) { + return; + } + + $request = $this->requestStack->getCurrentRequest(); + + if (!$request instanceof CliRequest || $request->command !== $event->getCommand()) { + return; + } + + if (null !== $sectionId = $request->attributes->get('_stopwatch_token')) { + // we must close the section before saving the profile to allow late collect + try { + $this->stopwatch->stopSection($sectionId); + } catch (\LogicException) { + // noop + } + } + + $request->command->exitCode = $event->getExitCode(); + $request->command->interruptedBySignal = $event->getInterruptingSignal(); + + $profile = $this->profiler->collect($request, $request->getResponse(), $this->error); + $this->error = null; + $this->profiles[$request] = $profile; + + if ($this->parents[$request] = $this->requestStack->getParentRequest()) { + // do not save on sub commands + return; + } + + // attach children to parents + foreach ($this->profiles as $request) { + if (null !== $parentRequest = $this->parents[$request]) { + if (isset($this->profiles[$parentRequest])) { + $this->profiles[$parentRequest]->addChild($this->profiles[$request]); + } + } + } + + $output = $event->getOutput(); + $output = $output instanceof ConsoleOutputInterface && $output->isVerbose() ? $output->getErrorOutput() : null; + + // save profiles + foreach ($this->profiles as $r) { + $p = $this->profiles[$r]; + $this->profiler->saveProfile($p); + + $token = $p->getToken(); + $output?->writeln(sprintf( + 'See profile %s', + $this->urlGenerator->generate('_profiler', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL), + $token + )); + } + + $this->profiles = new \SplObjectStorage(); + $this->parents = new \SplObjectStorage(); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php index df218013a2315..aa6d4e33c3466 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector; +use Symfony\Component\Console\DataCollector\CommandDataCollector; use Symfony\Component\HttpKernel\DataCollector\AjaxDataCollector; use Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector; use Symfony\Component\HttpKernel\DataCollector\EventDataCollector; @@ -30,7 +31,7 @@ ->set('data_collector.request', RequestDataCollector::class) ->args([ - service('request_stack')->ignoreOnInvalid(), + service('.virtual_request_stack')->ignoreOnInvalid(), ]) ->tag('kernel.event_subscriber') ->tag('data_collector', ['template' => '@WebProfiler/Collector/request.html.twig', 'id' => 'request', 'priority' => 335]) @@ -48,7 +49,7 @@ ->set('data_collector.events', EventDataCollector::class) ->args([ tagged_iterator('event_dispatcher.dispatcher', 'name'), - service('request_stack')->ignoreOnInvalid(), + service('.virtual_request_stack')->ignoreOnInvalid(), ]) ->tag('data_collector', ['template' => '@WebProfiler/Collector/events.html.twig', 'id' => 'events', 'priority' => 290]) @@ -56,7 +57,7 @@ ->args([ service('logger')->ignoreOnInvalid(), sprintf('%s/%s', param('kernel.build_dir'), param('kernel.container_class')), - service('request_stack')->ignoreOnInvalid(), + service('.virtual_request_stack')->ignoreOnInvalid(), ]) ->tag('monolog.logger', ['channel' => 'profiler']) ->tag('data_collector', ['template' => '@WebProfiler/Collector/logger.html.twig', 'id' => 'logger', 'priority' => 300]) @@ -74,5 +75,8 @@ ->set('data_collector.router', RouterDataCollector::class) ->tag('kernel.event_listener', ['event' => KernelEvents::CONTROLLER, 'method' => 'onKernelController']) ->tag('data_collector', ['template' => '@WebProfiler/Collector/router.html.twig', 'id' => 'router', 'priority' => 285]) + + ->set('.data_collector.command', CommandDataCollector::class) + ->tag('data_collector', ['template' => '@WebProfiler/Collector/command.html.twig', 'id' => 'command', 'priority' => 335]) ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php index cfaad8c1de241..d9341e16f7727 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver; use Symfony\Component\HttpKernel\Controller\TraceableControllerResolver; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; +use Symfony\Component\HttpKernel\Debug\VirtualRequestStack; return static function (ContainerConfigurator $container) { $container->services() @@ -24,7 +25,7 @@ service('debug.event_dispatcher.inner'), service('debug.stopwatch'), service('logger')->nullOnInvalid(), - service('request_stack')->nullOnInvalid(), + service('.virtual_request_stack')->nullOnInvalid(), ]) ->tag('monolog.logger', ['channel' => 'event']) ->tag('kernel.reset', ['method' => 'reset']) @@ -46,5 +47,9 @@ ->set('argument_resolver.not_tagged_controller', NotTaggedControllerValueResolver::class) ->args([abstract_arg('Controller argument, set in FrameworkExtension')]) ->tag('controller.argument_value_resolver', ['priority' => -200]) + + ->set('.virtual_request_stack', VirtualRequestStack::class) + ->args([service('request_stack')]) + ->public() ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php index 221217896fe93..c4b9f68a3b88a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/profiling.php @@ -11,6 +11,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Symfony\Bundle\FrameworkBundle\EventListener\ConsoleProfilerListener; use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; use Symfony\Component\HttpKernel\Profiler\Profiler; @@ -35,5 +36,14 @@ param('profiler_listener.only_main_requests'), ]) ->tag('kernel.event_subscriber') + + ->set('console_profiler_listener', ConsoleProfilerListener::class) + ->args([ + service('profiler'), + service('.virtual_request_stack'), + service('debug.stopwatch'), + service('router'), + ]) + ->tag('kernel.event_subscriber') ; }; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 83c8553b2706d..4411d59ba7ea9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; @@ -242,17 +243,25 @@ private function getKernel(array $bundles, $useDispatcher = false) { $container = $this->createMock(ContainerInterface::class); + $requestStack = $this->createMock(RequestStack::class); + $requestStack->expects($this->any()) + ->method('push') + ; + if ($useDispatcher) { $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher ->expects($this->atLeastOnce()) ->method('dispatch') ; - $container - ->expects($this->atLeastOnce()) + + $container->expects($this->atLeastOnce()) ->method('get') - ->with($this->equalTo('event_dispatcher')) - ->willReturn($dispatcher); + ->willReturnMap([ + ['.virtual_request_stack', 2, $requestStack], + ['event_dispatcher', 1, $dispatcher], + ]) + ; } $container diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index bdcfc3bdc5d3f..c3a2d8c8aab6e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.4 +--- + + * Add console commands to the profiler + 6.3 --- diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index aea579c0832ed..df36246cb7604 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -76,17 +76,20 @@ public function panelAction(Request $request, string $token): Response $panel = $request->query->get('panel'); $page = $request->query->get('page', 'home'); + $profileType = $request->query->get('type', 'request'); - if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null))) { + if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null, null, fn ($profile) => $profileType === $profile['virtual_type']))) { $token = $latest['token']; } if (!$profile = $this->profiler->loadProfile($token)) { - return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request]); + return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request, 'profile_type' => $profileType]); } + $profileType = $profile->getVirtualType() ?? 'request'; + if (null === $panel) { - $panel = 'request'; + $panel = $profileType; foreach ($profile->getCollectors() as $collector) { if ($collector instanceof ExceptionDataCollector && $collector->hasException()) { @@ -115,6 +118,7 @@ public function panelAction(Request $request, string $token): Response 'templates' => $this->getTemplateManager()->getNames($profile), 'is_ajax' => $request->isXmlHttpRequest(), 'profiler_markup_version' => 3, // 1 = original profiler, 2 = Symfony 2.8+ profiler, 3 = Symfony 6.2+ profiler + 'profile_type' => $profileType, ]); } @@ -192,6 +196,7 @@ public function searchBarAction(Request $request): Response 'limit' => $request->query->get('limit', $session?->get('_profiler_search_limit')), 'request' => $request, 'render_hidden_by_default' => false, + 'profile_type' => $request->query->get('type', $session?->get('_profiler_search_type', 'request')), ]), 200, ['Content-Type' => 'text/html'] @@ -218,12 +223,13 @@ public function searchResultsAction(Request $request, string $token): Response $start = $request->query->get('start', null); $end = $request->query->get('end', null); $limit = $request->query->get('limit'); + $profileType = $request->query->get('type', 'request'); return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/results.html.twig', [ 'request' => $request, 'token' => $token, 'profile' => $profile, - 'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode), + 'tokens' => $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, fn ($profile) => $profileType === $profile['virtual_type']), 'ip' => $ip, 'method' => $method, 'status_code' => $statusCode, @@ -232,6 +238,7 @@ public function searchResultsAction(Request $request, string $token): Response 'end' => $end, 'limit' => $limit, 'panel' => null, + 'profile_type' => $profileType, ]); } @@ -252,6 +259,7 @@ public function searchAction(Request $request): Response $end = $request->query->get('end', null); $limit = $request->query->get('limit'); $token = $request->query->get('token'); + $profileType = $request->query->get('type', 'request'); if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) { $session = $request->getSession(); @@ -264,13 +272,14 @@ public function searchAction(Request $request): Response $session->set('_profiler_search_end', $end); $session->set('_profiler_search_limit', $limit); $session->set('_profiler_search_token', $token); + $session->set('_profiler_search_type', $profileType); } if (!empty($token)) { return new RedirectResponse($this->generator->generate('_profiler', ['token' => $token]), 302, ['Content-Type' => 'text/html']); } - $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode); + $tokens = $this->profiler->find($ip, $url, $limit, $method, $start, $end, $statusCode, fn ($profile) => $profileType === $profile['virtual_type']); return new RedirectResponse($this->generator->generate('_profiler_search_results', [ 'token' => $tokens ? $tokens[0]['token'] : 'empty', @@ -281,6 +290,7 @@ public function searchAction(Request $request): Response 'start' => $start, 'end' => $end, 'limit' => $limit, + 'type' => $profileType, ]), 302, ['Content-Type' => 'text/html']); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/command.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/command.html.twig new file mode 100644 index 0000000000000..13ce9eb059e58 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/command.html.twig @@ -0,0 +1,249 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block menu %} + + {{ source('@WebProfiler/Icon/command.svg') }} + Console Command + +{% endblock %} + +{% block panel %} +

+ {% set command = collector.command %} + + {% if command.executor is defined %} + {{ command.executor|abbr_method }} + {% else %} + {{ command.class|abbr_class }} + {% endif %} + +

+ +
+
+

Command

+ +
+
+
+ {{ collector.duration }} + Duration +
+ +
+ {{ collector.maxMemoryUsage }} + Peak Memory Usage +
+ +
+ {{ collector.verbosityLevel }} + Verbosity Level +
+
+ +
+
+ {{ source('@WebProfiler/Icon/' ~ (collector.signalable is not empty ? 'yes' : 'no') ~ '.svg') }} + Signalable +
+ +
+ {{ source('@WebProfiler/Icon/' ~ (collector.interactive ? 'yes' : 'no') ~ '.svg') }} + Interactive +
+ +
+ {{ source('@WebProfiler/Icon/' ~ (collector.validateInput ? 'yes' : 'no') ~ '.svg') }} + Validate Input +
+ +
+ {{ source('@WebProfiler/Icon/' ~ (collector.enabled ? 'yes' : 'no') ~ '.svg') }} + Enabled +
+ +
+ {{ source('@WebProfiler/Icon/' ~ (collector.visible ? 'yes' : 'no') ~ '.svg') }} + Visible +
+
+ +

Arguments

+ + {% if collector.arguments is empty %} +
+

No arguments were set

+
+ {% else %} + {% include '@WebProfiler/Profiler/table.html.twig' with { data: collector.arguments, labels: ['Argument', 'Value'], maxDepth: 2 } only %} + {% endif %} + +

Options

+ + {% if collector.options is empty %} +
+

No options were set

+
+ {% else %} + {% include '@WebProfiler/Profiler/table.html.twig' with { data: collector.options, labels: ['Option', 'Value'], maxDepth: 2 } only %} + {% endif %} + + {% if collector.interactive %} +

Interactive Inputs

+ +

+ The values which have been set interactively. +

+ + {% if collector.interactiveInputs is empty %} +
+

No inputs were set

+
+ {% else %} + {% include '@WebProfiler/Profiler/table.html.twig' with { data: collector.interactiveInputs, labels: ['Input', 'Value'], maxDepth: 2 } only %} + {% endif %} + {% endif %} + +

Application inputs

+ + {% if collector.applicationInputs is empty %} +
+

No application inputs are set

+
+ {% else %} + {% include '@WebProfiler/Profiler/table.html.twig' with { data: collector.applicationInputs, labels: ['Input', 'Value'], maxDepth: 2 } only %} + {% endif %} +
+
+ +
+

Input / Output

+ +
+ + + + + + + + + +
Input{{ profiler_dump(collector.input) }}
Output{{ profiler_dump(collector.output) }}
+
+
+ +
+

Helper Set

+ +
+ {% if collector.helperSet is empty %} +
+

No helpers

+
+ {% else %} + + + + + + + + {% for helper in collector.helperSet|sort %} + + + + {% endfor %} + +
Helpers
{{ profiler_dump(helper) }}
+ {% endif %} +
+
+ +
+ {% set request_collector = profile.collectors.request %} +

Server Parameters

+
+

Server Parameters

+

Defined in .env

+ {{ include('@WebProfiler/Profiler/bag.html.twig', { bag: request_collector.dotenvvars }, with_context = false) }} + +

Defined as regular env variables

+ {% set requestserver = [] %} + {% for key, value in request_collector.requestserver|filter((_, key) => key not in request_collector.dotenvvars.keys) %} + {% set requestserver = requestserver|merge({(key): value}) %} + {% endfor %} + {{ include('@WebProfiler/Profiler/table.html.twig', { data: requestserver }, with_context = false) }} +
+
+ + {% if collector.signalable is not empty %} +
+

Signals

+ +
+

Subscribed signals

+ {{ collector.signalable|join(', ') }} + +

Handled signals

+ {% if collector.handledSignals is empty %} +
+

No signals handled

+
+ {% else %} + + + + + + + + + + + {% for signal, data in collector.handledSignals %} + + + + + + + {% endfor %} + +
SignalTimes handledTotal execution timeMemory peak
{{ signal }}{{ data.handled }}{{ data.duration }} ms{{ data.memory }} MiB
+ {% endif %} +
+
+ {% endif %} + + {% if profile.parent %} +
+

Parent Command

+ +
+

+ Return to parent command + (token = {{ profile.parent.token }}) +

+ + {{ profile.parent.url }} +
+
+ {% endif %} + + {% if profile.children|length %} +
+

Sub Commands {{ profile.children|length }}

+ +
+ {% for child in profile.children %} +

+ {{ child.url }} + (token = {{ child.token }}) +

+ {% endfor %} +
+
+ {% endif %} +
+{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig index 2fb4e0a848f35..3cca9851def05 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig @@ -103,7 +103,7 @@
{{ profile.children|length }} - Sub-Request{{ profile.children|length > 1 ? 's' }} + Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }}
{% set subrequests_time = has_time_events @@ -112,7 +112,7 @@
{{ subrequests_time }} ms - Sub-Request{{ profile.children|length > 1 ? 's' }} time + Sub-{{ profile_type|title }}{{ profile.children|length > 1 ? 's' }} time
{% endif %} @@ -143,24 +143,24 @@ {% if profile.parent %}

- Sub-Request {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }} + Sub-{{ profile_type|title }} {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }} {{ collector.events.__section__.duration }} ms - Return to parent request + Return to parent {{ profile_type }}

{% elseif profile.children|length > 0 %}

- Main Request {{ collector.events.__section__.duration }} ms + Main {{ profile_type|title }} {{ collector.events.__section__.duration }} ms

{% endif %} {{ _self.display_timeline(token, collector.events, collector.events.__section__.origin) }} {% if profile.children|length %} -

Note: sections with a striped background correspond to sub-requests.

+

Note: sections with a striped background correspond to sub-{{ profile_type }}s.

-

Sub-requests ({{ profile.children|length }})

+

Sub-{{ profile_type }}s ({{ profile.children|length }})

{% for child in profile.children %} {% set events = child.getcollector('time').events %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/command.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/command.svg new file mode 100644 index 0000000000000..fc391c7512bb6 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/command.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_command_summary.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_command_summary.html.twig new file mode 100644 index 0000000000000..218a869d3a6e3 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_command_summary.html.twig @@ -0,0 +1,44 @@ +{% set status_code = profile.statuscode|default(0) %} +{% set interrupted = command_collector is same as false ? null : command_collector.interruptedBySignal %} +{% set css_class = status_code == 113 or interrupted is not null ? 'status-warning' : status_code > 0 ? 'status-error' : 'status-success' %} + +
+
+ {% if status_code > 0 %} +

+ {{ source('@WebProfiler/Icon/alert-circle.svg') }} + Error ({{ status_code }}) +

+ {% endif %} + +

+ + {{ profile.method|upper }} + + + {{ profile.url|length < 160 ? profile.url : profile.url[:160] ~ '…' }} +

+ + +
+
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_request_summary.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_request_summary.html.twig new file mode 100644 index 0000000000000..45b687e13253a --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/_request_summary.html.twig @@ -0,0 +1,99 @@ +{% set status_code = request_collector ? request_collector.statuscode|default(0) : 0 %} +{% set css_class = status_code > 399 ? 'status-error' : status_code > 299 ? 'status-warning' : 'status-success' %} + +{% if request_collector and request_collector.redirect %} + {% set redirect = request_collector.redirect %} + {% set link_to_source_code = redirect.controller.class is defined ? redirect.controller.file|file_link(redirect.controller.line) %} + {% set redirect_route_name = '@' ~ redirect.route %} + +
+ {{ source('@WebProfiler/Icon/redirect.svg') }} + + {{ redirect.status_code }} redirect from + + {{ redirect.method }} + + {% if link_to_source_code %} + {{ redirect_route_name }} + {% else %} + {{ redirect_route_name }} + {% endif %} + + ({{ redirect.token }}) +
+{% endif %} + +
+ {% if status_code > 399 %} +

+ {{ source('@WebProfiler/Icon/alert-circle.svg') }} + Error {{ status_code }} + {{ request_collector.statusText }} +

+ {% endif %} + +

+ + {{ profile.method|upper }} + + + {% set profile_title = profile.url|length < 160 ? profile.url : profile.url[:160] ~ '…' %} + {% if profile.method|upper in ['GET', 'HEAD'] %} + {{ profile_title }} + {% else %} + {{ profile_title }} + {% endif %} +

+ + +
+ +{% if request_collector and request_collector.forwardtoken -%} + {% set forward_profile = profile.childByToken(request_collector.forwardtoken) %} + {% set controller = forward_profile ? forward_profile.collector('request').controller : 'n/a' %} +
+ {{ source('@WebProfiler/Icon/forward.svg') }} + + Forwarded to + + {% set link = controller.file is defined ? controller.file|file_link(controller.line) : null -%} + {%- if link %}{% endif -%} + {% if controller.class is defined %} + {{- controller.class|abbr_class|striptags -}} + {{- controller.method ? ' :: ' ~ controller.method -}} + {% else %} + {{- controller -}} + {% endif %} + {%- if link %}{% endif %} + ({{ request_collector.forwardtoken }}) + +
+{%- endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/header.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/header.html.twig index 9f5fc24fafc84..20049e5e95c64 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/header.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/header.html.twig @@ -1,6 +1,21 @@