Chris@13: Command took %.6f seconds to complete.'; Chris@13: const AVG_RESULT_MSG = 'Command took %.6f seconds on average (%.6f median; %.6f total) to complete.'; Chris@13: Chris@16: private static $start = null; Chris@16: private static $times = []; Chris@16: Chris@16: private $parser; Chris@16: private $traverser; Chris@16: private $printer; Chris@16: Chris@16: /** Chris@16: * {@inheritdoc} Chris@16: */ Chris@16: public function __construct($name = null) Chris@16: { Chris@16: $parserFactory = new ParserFactory(); Chris@16: $this->parser = $parserFactory->createParser(); Chris@16: Chris@16: $this->traverser = new NodeTraverser(); Chris@16: $this->traverser->addVisitor(new TimeitVisitor()); Chris@16: Chris@16: $this->printer = new Printer(); Chris@16: Chris@16: parent::__construct($name); Chris@16: } Chris@16: Chris@13: /** Chris@13: * {@inheritdoc} Chris@13: */ Chris@13: protected function configure() Chris@13: { Chris@13: $this Chris@13: ->setName('timeit') Chris@13: ->setDefinition([ Chris@13: new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Number of iterations.'), Chris@13: new CodeArgument('code', CodeArgument::REQUIRED, 'Code to execute.'), Chris@13: ]) Chris@13: ->setDescription('Profiles with a timer.') Chris@13: ->setHelp( Chris@13: <<<'HELP' Chris@13: Time profiling for functions and commands. Chris@13: Chris@13: e.g. Chris@13: >>> timeit sleep(1) Chris@13: >>> timeit -n1000 $closure() Chris@13: HELP Chris@13: ); Chris@13: } Chris@13: Chris@13: /** Chris@13: * {@inheritdoc} Chris@13: */ Chris@13: protected function execute(InputInterface $input, OutputInterface $output) Chris@13: { Chris@13: $code = $input->getArgument('code'); Chris@13: $num = $input->getOption('num') ?: 1; Chris@13: $shell = $this->getApplication(); Chris@13: Chris@16: $instrumentedCode = $this->instrumentCode($code); Chris@16: Chris@16: self::$times = []; Chris@16: Chris@13: for ($i = 0; $i < $num; $i++) { Chris@16: $_ = $shell->execute($instrumentedCode); Chris@16: $this->ensureEndMarked(); Chris@13: } Chris@13: Chris@13: $shell->writeReturnValue($_); Chris@13: Chris@16: $times = self::$times; Chris@16: self::$times = []; Chris@16: Chris@13: if ($num === 1) { Chris@17: $output->writeln(\sprintf(self::RESULT_MSG, $times[0])); Chris@13: } else { Chris@17: $total = \array_sum($times); Chris@17: \rsort($times); Chris@17: $median = $times[\round($num / 2)]; Chris@13: Chris@17: $output->writeln(\sprintf(self::AVG_RESULT_MSG, $total / $num, $median, $total)); Chris@13: } Chris@13: } Chris@16: Chris@16: /** Chris@16: * Internal method for marking the start of timeit execution. Chris@16: * Chris@16: * A static call to this method will be injected at the start of the timeit Chris@16: * input code to instrument the call. We will use the saved start time to Chris@16: * more accurately calculate time elapsed during execution. Chris@16: */ Chris@16: public static function markStart() Chris@16: { Chris@17: self::$start = \microtime(true); Chris@16: } Chris@16: Chris@16: /** Chris@16: * Internal method for marking the end of timeit execution. Chris@16: * Chris@16: * A static call to this method is injected by TimeitVisitor at the end Chris@16: * of the timeit input code to instrument the call. Chris@16: * Chris@16: * Note that this accepts an optional $ret parameter, which is used to pass Chris@16: * the return value of the last statement back out of timeit. This saves us Chris@16: * a bunch of code rewriting shenanigans. Chris@16: * Chris@16: * @param mixed $ret Chris@16: * Chris@16: * @return mixed it just passes $ret right back Chris@16: */ Chris@16: public static function markEnd($ret = null) Chris@16: { Chris@17: self::$times[] = \microtime(true) - self::$start; Chris@16: self::$start = null; Chris@16: Chris@16: return $ret; Chris@16: } Chris@16: Chris@16: /** Chris@16: * Ensure that the end of code execution was marked. Chris@16: * Chris@16: * The end *should* be marked in the instrumented code, but just in case Chris@16: * we'll add a fallback here. Chris@16: */ Chris@16: private function ensureEndMarked() Chris@16: { Chris@16: if (self::$start !== null) { Chris@16: self::markEnd(); Chris@16: } Chris@16: } Chris@16: Chris@16: /** Chris@16: * Instrument code for timeit execution. Chris@16: * Chris@16: * This inserts `markStart` and `markEnd` calls to ensure that (reasonably) Chris@16: * accurate times are recorded for just the code being executed. Chris@16: * Chris@16: * @param string $code Chris@16: * Chris@16: * @return string Chris@16: */ Chris@16: private function instrumentCode($code) Chris@16: { Chris@16: return $this->printer->prettyPrint($this->traverser->traverse($this->parse($code))); Chris@16: } Chris@16: Chris@16: /** Chris@16: * Lex and parse a string of code into statements. Chris@16: * Chris@16: * @param string $code Chris@16: * Chris@16: * @return array Statements Chris@16: */ Chris@16: private function parse($code) Chris@16: { Chris@16: $code = 'parser->parse($code); Chris@16: } catch (\PhpParser\Error $e) { Chris@17: if (\strpos($e->getMessage(), 'unexpected EOF') === false) { Chris@16: throw $e; Chris@16: } Chris@16: Chris@16: // If we got an unexpected EOF, let's try it again with a semicolon. Chris@16: return $this->parser->parse($code . ';'); Chris@16: } Chris@16: } Chris@13: }