Completed
Push — develop ( f8787b...f5c859 )
by Alec
03:03
created

Benchmark::refineIterations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\Tools;
4
5
use AlecRabbit\Accessories\MemoryUsage;
6
use AlecRabbit\Accessories\Rewindable;
7
use AlecRabbit\ConsoleColour\Terminal;
8
use AlecRabbit\Tools\Contracts\BenchmarkInterface;
9
use AlecRabbit\Tools\Contracts\Strings;
10
use AlecRabbit\Tools\Internal\BenchmarkFunction;
11
use AlecRabbit\Tools\Reports\BenchmarkReport;
12
use AlecRabbit\Tools\Traits\BenchmarkFields;
13
use function AlecRabbit\typeOf;
14
15
class Benchmark extends Reportable implements BenchmarkInterface, Strings
16
{
17
    use BenchmarkFields;
18
19
    public const MIN_ITERATIONS = 100;
20
    public const DEFAULT_STEPS = 100;
21
    public const CLOSURE_NAME = 'λ';
22
23
    public const DEFAULT_SEPARATOR_CHAR = '─';
24
    public const DEFAULT_END_SEPARATOR_CHAR = '═';
25
    public const DEFAULT_TERMINAL_WIDTH = 80;
26
    public const WIDTH_COEFFICIENT = 0.8;
27
28
    /** @var int */
29
    protected $advanceSteps = self::DEFAULT_STEPS;
30
    /** @var Rewindable */
31
    protected $rewindable;
32
    /** @var int */
33
    protected $iterations;
34
    /** @var null|string */
35
    protected $comment;
36
    /** @var string|null */
37
    protected $humanReadableName;
38
    /** @var int */
39
    protected $totalIterations = 0;
40
    /** @var null|callable */
41
    protected $onStart;
42
    /** @var null|callable */
43
    protected $onAdvance;
44
    /** @var null|callable */
45
    protected $onFinish;
46
    /** @var int */
47
    protected $advanceStep = 0;
48
    /** @var \Closure */
49
    protected $iterationNumberGenerator;
50
    /** @var bool */
51
    protected $launched = false;
52
    /** @var int */
53
    protected $functionIndex = 1;
54
    /** @var bool */
55
    protected $silent = false;
56
    /** @var int */
57
    protected $terminalWidth = self::DEFAULT_TERMINAL_WIDTH;
58
59
    /**
60
     * Benchmark constructor.
61
     * @param null|int $iterations
62
     * @param null|bool $silent
63
     * @throws \Exception
64
     */
65 20
    public function __construct(?int $iterations = null, ?bool $silent = null)
66
    {
67 20
        $this->iterations = $this->refineIterations($iterations);
68 20
        $this->silent = $silent ?? $this->silent;
69
70 20
        $this->iterationNumberGenerator =
71
            static function (int $iterations, int $i = 1): \Generator {
72 16
                while ($i <= $iterations) {
73 16
                    yield $i++;
74
                }
75 16
            };
76
77 20
        $this->timer = new Timer(); // Timer to count benchmark process total time
78 20
        $this->initialize();
79 20
        $this->terminalWidth = $this->terminalWidth();
80 20
    }
81
82 20
    protected function refineIterations(?int $iterations): int
83
    {
84 20
        $iterations = $iterations ?? self::MIN_ITERATIONS;
85 20
        $this->assertIterations($iterations);
86 20
        return $iterations;
87
    }
88
89
    /**
90
     * @param int $iterations
91
     */
92 20
    protected function assertIterations(int $iterations): void
93
    {
94 20
        if ($iterations < self::MIN_ITERATIONS) {
95 1
            throw new \RuntimeException(
96 1
                '[' . __CLASS__ . '] Number of Iterations should be greater than ' . self::MIN_ITERATIONS . '.'
97
            );
98
        }
99 20
    }
100
101
    /**
102
     * Resets Benchmark object clear
103
     * @throws \Exception
104
     */
105 20
    protected function initialize(): void
106
    {
107 20
        unset($this->functions, $this->humanReadableName, $this->rewindable, $this->memoryUsageReport);
108
109 20
        $this->rewindable =
110 20
            new Rewindable(
111 20
                $this->iterationNumberGenerator,
112 20
                $this->iterations
113
            );
114 20
        $this->humanReadableName = null;
115 20
        $this->functions = [];
116 20
        $this->functionIndex = 1;
117 20
        $this->launched = false;
118 20
        $this->resetComment();
119 20
        $this->added = new SimpleCounter('added');
120 20
        $this->benchmarked = new SimpleCounter('benchmarked');
121 20
        $this->memoryUsageReport = MemoryUsage::report();
122 20
        $this->doneIterations = 0;
123 20
        $this->totalIterations = 0;
124 20
        $this->report = (new BenchmarkReport())->buildOn($this);
125 20
    }
126
127 20
    protected function resetComment(): void
128
    {
129 20
        $this->comment = null;
130 20
    }
131
132
    /**
133
     * @return int
134
     */
135 20
    protected function terminalWidth(): int
136
    {
137 20
        return (int)((new Terminal())->width() * static::WIDTH_COEFFICIENT);
138
    }
139
140
    /**
141
     * Resets Benchmark object clear. Returns divider string.
142
     * @param null|string $char
143
     * @return string
144
     * @throws \Exception
145
     */
146 2
    public function reset(?string $char = null): string
147
    {
148 2
        $this->initialize();
149
        return
150 2
            $this->sectionSeparator($char);
151
    }
152
153 2
    protected function sectionSeparator(?string $char): string
154
    {
155 2
        return str_repeat($char ?? static::DEFAULT_SEPARATOR_CHAR, self::DEFAULT_TERMINAL_WIDTH) . PHP_EOL . PHP_EOL;
156
    }
157
158
    /**
159
     * @param callable|null $onStart
160
     * @param callable|null $onAdvance
161
     * @param callable|null $onFinish
162
     * @return Benchmark
163
     */
164 3
    public function showProgressBy(
165
        callable $onStart = null,
166
        callable $onAdvance = null,
167
        callable $onFinish = null
168
    ): Benchmark {
169 3
        $this->onStart = $onStart;
170 3
        $this->onAdvance = $onAdvance;
171 3
        $this->onFinish = $onFinish;
172 3
        return $this;
173
    }
174
175
    /**
176
     * @param mixed $func
177
     * @param mixed ...$args
178
     * @throws \Exception
179
     */
180 17
    public function addFunction($func, ...$args): void
181
    {
182 17
        if ($this->isLaunched()) {
183
            throw new \RuntimeException('You should reset benchmark object before adding a new function.');
184
        }
185 17
        if (!\is_callable($func, false, $name)) {
186 1
            throw new \InvalidArgumentException(
187 1
                sprintf(
188 1
                    '\'%s\' is NOT callable. Function must be callable. Type of "%s" provided instead.',
189
                    $name,
190 1
                    typeOf($func)
191
                )
192
            );
193
        }
194
        $function =
195 16
            new BenchmarkFunction(
196 16
                $func,
197 16
                $this->refineName($func, $name),
198 16
                $this->functionIndex++,
199
                $args,
200 16
                $this->comment,
201 16
                $this->humanReadableName
202
            );
203 16
        $function->setShowReturns($this->isShowReturns());
204 16
        $this->functions[$function->enumeratedName()] = $function;
205 16
        $this->humanReadableName = null;
206 16
        $this->resetComment();
207 16
        $this->added->bump();
208 16
        $this->totalIterations += $this->iterations;
209 16
    }
210
211
    /**
212
     * @return bool
213
     */
214 18
    public function isLaunched(): bool
215
    {
216 18
        return $this->launched;
217
    }
218
219
    /**
220
     * @param callable $func
221
     * @param string $name
222
     * @return string
223
     */
224 16
    protected function refineName($func, $name): string
225
    {
226 16
        if ($func instanceof \Closure) {
227 16
            $name = self::CLOSURE_NAME;
228
        }
229 16
        return $name;
230
    }
231
232
    /**
233
     * @param string $comment
234
     * @return Benchmark
235
     */
236 6
    public function withComment(string $comment): self
237
    {
238 6
        $this->comment = $comment;
239 6
        return $this;
240
    }
241
242
    /**
243
     * @param string $name
244
     * @return Benchmark
245
     */
246 7
    public function useName(string $name): self
247
    {
248 7
        $this->humanReadableName = $name;
249 7
        return $this;
250
    }
251
252
    /**
253
     * @param bool $eol
254
     * @return string
255
     * @throws \Exception
256
     */
257 4
    public function stat(bool $eol = true): string
258
    {
259
        return
260 4
            sprintf(
261 4
                'Done in: %s%s%s',
262 4
                $this->getTimer()->elapsed(),
263 4
                PHP_EOL,
264 4
                (string)$this->memoryUsageReport
265
            ) .
266 4
            ($eol ? PHP_EOL : '');
267
    }
268
269
    /**
270
     * @return Benchmark
271
     */
272 1
    public function showReturns(): Benchmark
273
    {
274 1
        $this->setShowReturns(true);
275 1
        foreach ($this->functions as $function) {
276 1
            $function->setShowReturns($this->isShowReturns());
277
        }
278 1
        return $this;
279
    }
280
281
    /**
282
     * @param bool $showReturns
283
     */
284 1
    public function setShowReturns(bool $showReturns): void
285
    {
286 1
        $this->showReturns = $showReturns;
287 1
    }
288
289
    /** {@inheritdoc} */
290 15
    protected function meetConditions(): void
291
    {
292 15
        if ($this->isNotLaunched()) {
293 10
            $this->run();
294
        }
295 15
    }
296
297
    /**
298
     * @return bool
299
     */
300 15
    public function isNotLaunched(): bool
301
    {
302 15
        return !$this->isLaunched();
303
    }
304
305
    /**
306
     * Launch benchmarking
307
     */
308 17
    public function run(): self
309
    {
310 17
        $this->displayComment();
311 17
        $this->launched = true;
312 17
        if ($this->onStart) {
313 3
            ($this->onStart)();
314
        }
315 17
        $this->execute();
316 17
        if ($this->onFinish) {
317 3
            ($this->onFinish)();
318
        }
319 17
        $this->doneIterationsCombined += $this->doneIterations;
320 17
        return $this;
321
    }
322
323 17
    protected function displayComment(): void
324
    {
325 17
        if (!$this->silent && null !== $this->comment) {
326
            $this->showComment($this->comment);
327
            $this->resetComment();
328
        }
329 17
    }
330
331
    protected function showComment(string $comment = ''): void
332
    {
333
        echo $comment . PHP_EOL;
334
    }
335
336
    /**
337
     * Benchmarking
338
     */
339 17
    protected function execute(): void
340
    {
341
        /** @var  BenchmarkFunction $f */
342 17
        foreach ($this->functions as $f) {
343 16
            if (!$f->execute()) {
344 7
                $this->totalIterations -= $this->iterations;
345 7
                continue;
346
            }
347 16
            $this->advanceStep = (int)($this->totalIterations / $this->advanceSteps + 1);
348 16
            $this->bench($f);
349 16
            $this->benchmarked->bump();
350
        }
351 17
    }
352
353
    /**
354
     * @param BenchmarkFunction $f
355
     */
356 16
    protected function bench(BenchmarkFunction $f): void
357
    {
358 16
        $timer = $f->getTimer();
359 16
        $function = $f->getCallable();
360 16
        $args = $f->getArgs();
361 16
        foreach ($this->rewindable as $iteration) {
362 16
            $start = $timer->current();
363
            /** @noinspection DisconnectedForeachInstructionInspection */
364 16
            $function(...$args);
365 16
            $stop = $timer->current();
366 16
            $timer->bounds($start, $stop, $iteration);
367
            /** @noinspection DisconnectedForeachInstructionInspection */
368 16
            $this->progress();
369
        }
370 16
    }
371
372 16
    protected function progress(): void
373
    {
374 16
        $this->doneIterations++;
375 16
        if ($this->onAdvance && 0 === $this->doneIterations % $this->advanceStep) {
376 3
            ($this->onAdvance)();
377
        }
378 16
    }
379
}
380