Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 200 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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