Subversion-Projekte lars-tiefland.laravel_shop

Revision

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