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