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