Passed
Push — develop ( c56c81...f741b1 )
by Alec
03:32
created

Benchmark::useName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
ccs 0
cts 6
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace AlecRabbit\Tools;
4
5
use AlecRabbit\Exception\InvalidStyleException;
6
use AlecRabbit\Rewindable;
7
use AlecRabbit\Tools\Contracts\BenchmarkInterface;
8
use AlecRabbit\Tools\Internal\BenchmarkFunction;
9
use AlecRabbit\Tools\Reports\Contracts\ReportableInterface;
10
use AlecRabbit\Tools\Reports\Factory;
11
use AlecRabbit\Tools\Reports\Formatters\Helper;
12
use AlecRabbit\Tools\Reports\Traits\Reportable;
13
use AlecRabbit\Tools\Traits\BenchmarkFields;
14
use function AlecRabbit\brackets;
15
use function AlecRabbit\typeOf;
16
17
class Benchmark implements BenchmarkInterface, ReportableInterface
18
{
19
    protected const PG_WIDTH = 60;
20
    protected const ADDED = 'added';
21
    protected const BENCHMARKED = 'benchmarked';
22
23
    use BenchmarkFields, Reportable;
24
25
    /** @var int */
26
    private $namingIndex = 1;
27
    /** @var Rewindable */
28
    private $rewindable;
29
    /** @var int */
30
    private $iterations;
31
    /** @var null|string */
32
    private $comment;
33
    /** @var bool */
34
    private $verbose;
35
    /** @var int */
36
    private $dots;
37
    /** @var array */
38
    private $names;
39
    /** @var string|null */
40
    private $humanReadableName;
41
    /** @var int */
42
    private $iterationsToBench;
43
44
    /**
45
     * Benchmark constructor.
46
     * @param int $iterations
47
     */
48 5
    public function __construct(int $iterations = 1000)
49
    {
50 5
        $this->iterations = $iterations;
51 5
        $this->reset();
52 5
    }
53
54
    /**
55
     * Resets Benchmark object clear
56
     */
57 5
    public function reset(): void
58
    {
59 5
        $this->names = [];
60 5
        $this->humanReadableName = null;
61 5
        $this->dots = 0;
62 5
        $this->verbose = false;
63 5
        $this->rewindable =
64 5
            new Rewindable(
65
                function (int $iterations, int $i = 1): \Generator {
66 2
                    while ($i <= $iterations) {
67 2
                        yield $i++;
68
                    }
69 5
                },
70 5
                $this->iterations
71
            );
72 5
        $this->resetFields();
73 5
        $this->resetReportObject();
74 5
    }
75
76
    /**
77
     * Launch benchmarking
78
     * @param bool $printReport
79
     * @throws InvalidStyleException
80
     */
81 2
    public function run(bool $printReport = false): void
82
    {
83 2
        if ($this->verbose) {
84
            echo
85 1
            sprintf(
86 1
                'Running benchmarks(Functions: %s, Repeat: %s):',
87 1
                $this->profiler->counter(self::ADDED)->getValue(),
88 1
                $this->iterations
89
            );
90 1
            echo PHP_EOL;
91
        }
92 2
        $this->execute();
93
94 2
        if ($this->verbose) {
95 1
            $this->erase();
96 1
            echo ' 100%' . PHP_EOL;
97 1
            echo '  λ   Done!' . PHP_EOL;
98
        }
99
100 2
        if ($printReport) {
101 1
            echo PHP_EOL;
102 1
            echo (string)$this->getReport();
103 1
            echo PHP_EOL;
104
        }
105 2
    }
106
107
    /**
108
     * Benchmarking
109
     */
110 2
    private function execute(): void
111
    {
112
        /** @var  BenchmarkFunction $f */
113 2
        foreach ($this->functions as $name => $f) {
114 2
            $function = $f->getFunction();
115 2
            $args = $f->getArgs();
116 2
            $this->prepareResult($f, $function, $args);
117 2
            $timer = $f->getTimer();
118 2
            if ($f->getException()) {
119 1
                $timer->check();
120 1
                $this->iterationsToBench -= $this->iterations;
121 1
                continue;
122
            }
123 2
            foreach ($this->rewindable as $iteration) {
124 2
                $this->bench($timer, $function, $args, $iteration);
125
            }
126 2
            $this->profiler->counter(self::BENCHMARKED)->bump();
127
        }
128 2
    }
129
130
    /**
131
     * @param BenchmarkFunction $f
132
     * @param callable $function
133
     * @param array $args
134
     */
135 2
    private function prepareResult(BenchmarkFunction $f, callable $function, array $args): void
136
    {
137
        try {
138 2
            $result = $function(...$args);
139 1
        } catch (\Throwable $e) {
140 1
            $this->exceptionMessages[$f->getIndexedName()] = $result = brackets(typeOf($e)) . ': ' . $e->getMessage();
141 1
            $this->exceptions[$f->getIndexedName()] = $e;
142 1
            $f->setException($e);
143
        }
144 2
        $f->setResult($result);
145 2
    }
146
147
    /**
148
     * @param Timer $timer
149
     * @param callable $function
150
     * @param array $args
151
     * @param int $iteration
152
     */
153 2
    private function bench(Timer $timer, callable $function, array $args, int $iteration): void
154
    {
155 2
        $timer->start();
156 2
        $function(...$args);
157 2
        $timer->check($iteration);
158 2
        $this->progress();
159 2
    }
160
161 2
    private function progress(): void
162
    {
163 2
        if ($this->verbose && 1 === ++$this->totalIterations % 5000) {
164 1
            $this->erase();
165 1
            echo '.';
166
            $a =
167 1
                str_pad(
168 1
                    Helper::percent($this->totalIterations / $this->iterationsToBench),
169 1
                    6,
170 1
                    ' ',
171 1
                    STR_PAD_LEFT
172
                );
173 1
            echo $a;
174 1
            if (++$this->dots > static::PG_WIDTH) {
175
                echo PHP_EOL;
176
                $this->dots = 0;
177
            }
178
        }
179 2
    }
180
181 1
    private function erase(): void
182
    {
183 1
        echo "\e[6D";
184 1
    }
185
186
    /**
187
     * @return Benchmark
188
     */
189 1
    public function verbose(): self
190
    {
191 1
        $this->verbose = true;
192 1
        return $this;
193
    }
194
195
    /**
196
     * @return Benchmark
197
     */
198 2
    public function color(): self
199
    {
200 2
        Factory::enableColour(true);
201 2
        return $this;
202
    }
203
204
    /**
205
     * @param callable $func
206
     * @param mixed ...$args
207
     */
208 3
    public function addFunction($func, ...$args): void
209
    {
210 3
        if (!\is_callable($func, false, $name)) {
211 1
            throw new \InvalidArgumentException(
212 1
                sprintf(
213 1
                    '\'%s\' is NOT callable. Function must be callable. Type of "%s" provided instead.',
214 1
                    $name,
215 1
                    typeOf($func)
216
                )
217
            );
218
        }
219
        $function =
220 2
            new BenchmarkFunction(
221 2
                $func,
222 2
                $this->refineName($func, $name),
223 2
                $this->namingIndex++,
224 2
                $args,
225 2
                $this->comment,
226 2
                $this->humanReadableName
227
            );
228 2
        $this->functions[$function->enumeratedName()] = $function;
229 2
        $this->humanReadableName = null;
230 2
        $this->comment = null;
231 2
        $this->profiler->counter(self::ADDED)->bump();
232 2
        $this->iterationsToBench += $this->iterations;
233 2
    }
234
235
    /**
236
     * @param callable $func
237
     * @param string $name
238
     * @return string
239
     */
240 2
    private function refineName($func, $name): string
241
    {
242 2
        if ($func instanceof \Closure) {
243 2
            $name = 'λ';
244
        }
245 2
        return $name;
246
    }
247
248
    /**
249
     * @param string $comment
250
     * @return Benchmark
251
     */
252 2
    public function withComment(string $comment): self
253
    {
254 2
        $this->comment = $comment;
255 2
        return $this;
256
    }
257
258
    /**
259
     * @param string $name
260
     * @return Benchmark
261
     */
262
    public function useName(string $name): self
263
    {
264
        if (in_array($name, $this->names, true)) {
265
            throw new \InvalidArgumentException(sprintf('Name "%s" is not unique', $name));
266
        }
267
        $this->names[] = $name;
268
        $this->humanReadableName = $name;
269
        return $this;
270
    }
271
272
    /**
273
     * @return Benchmark
274
     */
275 1
    public function returnResults(): self
276
    {
277 1
        $this->withResults = true;
278 1
        return $this;
279
    }
280
281
    /**
282
     * @return string
283
     * @throws \Throwable
284
     */
285 1
    public function elapsed(): string
286
    {
287 1
        $theme = Factory::getThemedObject();
288
        return
289 1
            sprintf(
290 1
                'Done in: %s',
291 1
                $theme->yellow($this->getProfiler()->timer()->elapsed())
292
            );
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 3
    protected function prepareForReport(): void
299
    {
300 3
        $this->getProfiler()->getReport();
301 3
    }
302
}
303