Skip to content

Commit d9c21b1

Browse files
committed
[Console][FrameworkBundle][HttpKernel][WebProfilerBundle] Enable profiling commands
1 parent 2edf780 commit d9c21b1

File tree

4 files changed

+663
-0
lines changed

4 files changed

+663
-0
lines changed

Command/TraceableCommand.php

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
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\Component\Console\Command;
13+
14+
use Symfony\Component\Console\Application;
15+
use Symfony\Component\Console\Completion\CompletionInput;
16+
use Symfony\Component\Console\Completion\CompletionSuggestions;
17+
use Symfony\Component\Console\Helper\HelperInterface;
18+
use Symfony\Component\Console\Helper\HelperSet;
19+
use Symfony\Component\Console\Input\InputDefinition;
20+
use Symfony\Component\Console\Input\InputInterface;
21+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Stopwatch\Stopwatch;
24+
25+
/**
26+
* @internal
27+
*
28+
* @author Jules Pietri <[email protected]>
29+
*/
30+
final class TraceableCommand extends Command implements SignalableCommandInterface
31+
{
32+
public readonly Command $command;
33+
public int $exitCode;
34+
public ?int $interruptedBySignal = null;
35+
public bool $ignoreValidation;
36+
public bool $isInteractive = false;
37+
public string $duration = 'n/a';
38+
public string $maxMemoryUsage = 'n/a';
39+
public InputInterface $input;
40+
public OutputInterface $output;
41+
/** @var array<string, mixed> */
42+
public array $arguments;
43+
/** @var array<string, mixed> */
44+
public array $options;
45+
/** @var array<string, mixed> */
46+
public array $interactiveInputs = [];
47+
public array $handledSignals = [];
48+
49+
public function __construct(
50+
Command $command,
51+
private readonly Stopwatch $stopwatch,
52+
) {
53+
if ($command instanceof LazyCommand) {
54+
$command = $command->getCommand();
55+
}
56+
57+
$this->command = $command;
58+
59+
// prevent call to self::getDefaultDescription()
60+
$this->setDescription($command->getDescription());
61+
62+
parent::__construct($command->getName());
63+
64+
// init below enables calling {@see parent::run()}
65+
[$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(function () {
66+
return [$this->code, $this->processTitle, $this->ignoreValidationErrors];
67+
}, $command, Command::class)();
68+
69+
if (\is_callable($code)) {
70+
$this->setCode($code);
71+
}
72+
73+
if ($processTitle) {
74+
parent::setProcessTitle($processTitle);
75+
}
76+
77+
if ($ignoreValidationErrors) {
78+
parent::ignoreValidationErrors();
79+
}
80+
81+
$this->ignoreValidation = $ignoreValidationErrors;
82+
}
83+
84+
public function __call(string $name, array $arguments): mixed
85+
{
86+
return $this->command->{$name}(...$arguments);
87+
}
88+
89+
public function getSubscribedSignals(): array
90+
{
91+
return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
92+
}
93+
94+
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
95+
{
96+
if (!$this->command instanceof SignalableCommandInterface) {
97+
return false;
98+
}
99+
100+
$event = $this->stopwatch->start($this->getName().'.handle_signal');
101+
102+
$exit = $this->command->handleSignal($signal, $previousExitCode);
103+
104+
$event->stop();
105+
106+
if (!isset($this->handledSignals[$signal])) {
107+
$this->handledSignals[$signal] = [
108+
'handled' => 0,
109+
'duration' => 0,
110+
'memory' => 0,
111+
];
112+
}
113+
114+
++$this->handledSignals[$signal]['handled'];
115+
$this->handledSignals[$signal]['duration'] += $event->getDuration();
116+
$this->handledSignals[$signal]['memory'] = max(
117+
$this->handledSignals[$signal]['memory'],
118+
$event->getMemory() >> 20
119+
);
120+
121+
return $exit;
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*
127+
* Calling parent method is required to be used in {@see parent::run()}.
128+
*/
129+
public function ignoreValidationErrors(): void
130+
{
131+
$this->ignoreValidation = true;
132+
$this->command->ignoreValidationErrors();
133+
134+
parent::ignoreValidationErrors();
135+
}
136+
137+
public function setApplication(Application $application = null): void
138+
{
139+
$this->command->setApplication($application);
140+
}
141+
142+
public function getApplication(): ?Application
143+
{
144+
return $this->command->getApplication();
145+
}
146+
147+
public function setHelperSet(HelperSet $helperSet): void
148+
{
149+
$this->command->setHelperSet($helperSet);
150+
}
151+
152+
public function getHelperSet(): ?HelperSet
153+
{
154+
return $this->command->getHelperSet();
155+
}
156+
157+
public function isEnabled(): bool
158+
{
159+
return $this->command->isEnabled();
160+
}
161+
162+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
163+
{
164+
$this->command->complete($input, $suggestions);
165+
}
166+
167+
/**
168+
* {@inheritdoc}
169+
*
170+
* Calling parent method is required to be used in {@see parent::run()}.
171+
*/
172+
public function setCode(callable $code): static
173+
{
174+
$this->command->setCode($code);
175+
176+
return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
177+
$event = $this->stopwatch->start($this->getName().'.code');
178+
179+
$this->exitCode = $code($input, $output);
180+
181+
$event->stop();
182+
183+
return $this->exitCode;
184+
});
185+
}
186+
187+
/**
188+
* @internal
189+
*/
190+
public function mergeApplicationDefinition(bool $mergeArgs = true): void
191+
{
192+
$this->command->mergeApplicationDefinition($mergeArgs);
193+
}
194+
195+
public function setDefinition(array|InputDefinition $definition): static
196+
{
197+
$this->command->setDefinition($definition);
198+
199+
return $this;
200+
}
201+
202+
public function getDefinition(): InputDefinition
203+
{
204+
return $this->command->getDefinition();
205+
}
206+
207+
public function getNativeDefinition(): InputDefinition
208+
{
209+
return $this->command->getNativeDefinition();
210+
}
211+
212+
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
213+
{
214+
$this->command->addArgument($name, $mode, $description, $default, $suggestedValues);
215+
216+
return $this;
217+
}
218+
219+
public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
220+
{
221+
$this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
222+
223+
return $this;
224+
}
225+
226+
/**
227+
* {@inheritdoc}
228+
*
229+
* Calling parent method is required to be used in {@see parent::run()}.
230+
*/
231+
public function setProcessTitle(string $title): static
232+
{
233+
$this->command->setProcessTitle($title);
234+
235+
return parent::setProcessTitle($title);
236+
}
237+
238+
public function setHelp(string $help): static
239+
{
240+
$this->command->setHelp($help);
241+
242+
return $this;
243+
}
244+
245+
public function getHelp(): string
246+
{
247+
return $this->command->getHelp();
248+
}
249+
250+
public function getProcessedHelp(): string
251+
{
252+
return $this->command->getProcessedHelp();
253+
}
254+
255+
public function getSynopsis(bool $short = false): string
256+
{
257+
return $this->command->getSynopsis($short);
258+
}
259+
260+
public function addUsage(string $usage): static
261+
{
262+
$this->command->addUsage($usage);
263+
264+
return $this;
265+
}
266+
267+
public function getUsages(): array
268+
{
269+
return $this->command->getUsages();
270+
}
271+
272+
public function getHelper(string $name): HelperInterface
273+
{
274+
return $this->command->getHelper($name);
275+
}
276+
277+
public function run(InputInterface $input, OutputInterface $output): int
278+
{
279+
$this->input = $input;
280+
$this->output = $output;
281+
$this->arguments = $input->getArguments();
282+
$this->options = $input->getOptions();
283+
$event = $this->stopwatch->start($this->getName(), 'command');
284+
285+
try {
286+
$this->exitCode = parent::run($input, $output);
287+
} finally {
288+
$event->stop();
289+
290+
if ($output instanceof ConsoleOutputInterface && $output->isDebug()) {
291+
$output->getErrorOutput()->writeln((string) $event);
292+
}
293+
294+
$this->duration = $event->getDuration().' ms';
295+
$this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';
296+
297+
if ($this->isInteractive) {
298+
$this->extractInteractiveInputs($input->getArguments(), $input->getOptions());
299+
}
300+
}
301+
302+
return $this->exitCode;
303+
}
304+
305+
protected function initialize(InputInterface $input, OutputInterface $output): void
306+
{
307+
$event = $this->stopwatch->start($this->getName().'.init', 'command');
308+
309+
$this->command->initialize($input, $output);
310+
311+
$event->stop();
312+
}
313+
314+
protected function interact(InputInterface $input, OutputInterface $output): void
315+
{
316+
if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) {
317+
return;
318+
}
319+
320+
$event = $this->stopwatch->start($this->getName().'.interact', 'command');
321+
322+
$this->command->interact($input, $output);
323+
324+
$event->stop();
325+
}
326+
327+
protected function execute(InputInterface $input, OutputInterface $output): int
328+
{
329+
$event = $this->stopwatch->start($this->getName().'.execute', 'command');
330+
331+
$exitCode = $this->command->execute($input, $output);
332+
333+
$event->stop();
334+
335+
return $exitCode;
336+
}
337+
338+
private function extractInteractiveInputs(array $arguments, array $options): void
339+
{
340+
foreach ($arguments as $argName => $argValue) {
341+
if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) {
342+
continue;
343+
}
344+
345+
$this->interactiveInputs[$argName] = $argValue;
346+
}
347+
348+
foreach ($options as $optName => $optValue) {
349+
if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) {
350+
continue;
351+
}
352+
353+
$this->interactiveInputs['--'.$optName] = $optValue;
354+
}
355+
}
356+
}

0 commit comments

Comments
 (0)