annotate vendor/psy/psysh/src/Command/TimeitCommand.php @ 19:fa3358dc1485 tip

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