comparison vendor/psy/psysh/src/Command/TimeitCommand.php @ 0:c75dbcec494b

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