Нема описа

TimeitCommand.php 5.2KB

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