c59663131132c3d041991e2f1038ef5fba2c3c2e
[yaffs-website] / vendor / psy / psysh / src / Command / TimeitCommand.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2018 Justin Hileman
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 Psy\Command;
13
14 use PhpParser\NodeTraverser;
15 use PhpParser\PrettyPrinter\Standard as Printer;
16 use Psy\Command\TimeitCommand\TimeitVisitor;
17 use Psy\Input\CodeArgument;
18 use Psy\ParserFactory;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\InputOption;
21 use Symfony\Component\Console\Output\OutputInterface;
22
23 /**
24  * Class TimeitCommand.
25  */
26 class TimeitCommand extends Command
27 {
28     const RESULT_MSG     = '<info>Command took %.6f seconds to complete.</info>';
29     const AVG_RESULT_MSG = '<info>Command took %.6f seconds on average (%.6f median; %.6f total) to complete.</info>';
30
31     private static $start = null;
32     private static $times = [];
33
34     private $parser;
35     private $traverser;
36     private $printer;
37
38     /**
39      * {@inheritdoc}
40      */
41     public function __construct($name = null)
42     {
43         $parserFactory = new ParserFactory();
44         $this->parser = $parserFactory->createParser();
45
46         $this->traverser = new NodeTraverser();
47         $this->traverser->addVisitor(new TimeitVisitor());
48
49         $this->printer = new Printer();
50
51         parent::__construct($name);
52     }
53
54     /**
55      * {@inheritdoc}
56      */
57     protected function configure()
58     {
59         $this
60             ->setName('timeit')
61             ->setDefinition([
62                 new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Number of iterations.'),
63                 new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'),
64             ])
65             ->setDescription('Profiles with a timer.')
66             ->setHelp(
67                 <<<'HELP'
68 Time profiling for functions and commands.
69
70 e.g.
71 <return>>>> timeit sleep(1)</return>
72 <return>>>> timeit -n1000 $closure()</return>
73 HELP
74             );
75     }
76
77     /**
78      * {@inheritdoc}
79      */
80     protected function execute(InputInterface $input, OutputInterface $output)
81     {
82         $code = $input->getArgument('code');
83         $num = $input->getOption('num') ?: 1;
84         $shell = $this->getApplication();
85
86         $instrumentedCode = $this->instrumentCode($code);
87
88         self::$times = [];
89
90         for ($i = 0; $i < $num; $i++) {
91             $_ = $shell->execute($instrumentedCode);
92             $this->ensureEndMarked();
93         }
94
95         $shell->writeReturnValue($_);
96
97         $times = self::$times;
98         self::$times = [];
99
100         if ($num === 1) {
101             $output->writeln(\sprintf(self::RESULT_MSG, $times[0]));
102         } else {
103             $total = \array_sum($times);
104             \rsort($times);
105             $median = $times[\round($num / 2)];
106
107             $output->writeln(\sprintf(self::AVG_RESULT_MSG, $total / $num, $median, $total));
108         }
109     }
110
111     /**
112      * Internal method for marking the start of timeit execution.
113      *
114      * A static call to this method will be injected at the start of the timeit
115      * input code to instrument the call. We will use the saved start time to
116      * more accurately calculate time elapsed during execution.
117      */
118     public static function markStart()
119     {
120         self::$start = \microtime(true);
121     }
122
123     /**
124      * Internal method for marking the end of timeit execution.
125      *
126      * A static call to this method is injected by TimeitVisitor at the end
127      * of the timeit input code to instrument the call.
128      *
129      * Note that this accepts an optional $ret parameter, which is used to pass
130      * the return value of the last statement back out of timeit. This saves us
131      * a bunch of code rewriting shenanigans.
132      *
133      * @param mixed $ret
134      *
135      * @return mixed it just passes $ret right back
136      */
137     public static function markEnd($ret = null)
138     {
139         self::$times[] = \microtime(true) - self::$start;
140         self::$start = null;
141
142         return $ret;
143     }
144
145     /**
146      * Ensure that the end of code execution was marked.
147      *
148      * The end *should* be marked in the instrumented code, but just in case
149      * we'll add a fallback here.
150      */
151     private function ensureEndMarked()
152     {
153         if (self::$start !== null) {
154             self::markEnd();
155         }
156     }
157
158     /**
159      * Instrument code for timeit execution.
160      *
161      * This inserts `markStart` and `markEnd` calls to ensure that (reasonably)
162      * accurate times are recorded for just the code being executed.
163      *
164      * @param string $code
165      *
166      * @return string
167      */
168     private function instrumentCode($code)
169     {
170         return $this->printer->prettyPrint($this->traverser->traverse($this->parse($code)));
171     }
172
173     /**
174      * Lex and parse a string of code into statements.
175      *
176      * @param string $code
177      *
178      * @return array Statements
179      */
180     private function parse($code)
181     {
182         $code = '<?php ' . $code;
183
184         try {
185             return $this->parser->parse($code);
186         } catch (\PhpParser\Error $e) {
187             if (\strpos($e->getMessage(), 'unexpected EOF') === false) {
188                 throw $e;
189             }
190
191             // If we got an unexpected EOF, let's try it again with a semicolon.
192             return $this->parser->parse($code . ';');
193         }
194     }
195 }