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: }