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