Completed
Push — master ( 85337f...00c842 )
by Alec
03:22
created

Benchmark::assertIterations()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 2
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
    /**
83
     * @return int
84
     */
85 20
    protected function terminalWidth(): int
86
    {
87 20
        return (int)((new Terminal())->width() * static::WIDTH_COEFFICIENT);
88
    }
89
90 20
    protected function refineIterations(?int $iterations): int
91
    {
92 20
        $iterations = $iterations ?? self::MIN_ITERATIONS;
93 20
        $this->assertIterations($iterations);
94 20
        return $iterations;
95
    }
96
97
    /**
98
     * @param int $iterations
99
     */
100 20
    protected function assertIterations(int $iterations): void
101
    {
102 20
        if ($iterations < self::MIN_ITERATIONS) {
103 1
            throw new \RuntimeException(
104 1
                '[' . __CLASS__ . '] Number of Iterations should be greater than ' . self::MIN_ITERATIONS . '.'
105
            );
106
        }
107 20
    }
108
109
    /**
110
     * Resets Benchmark object clear
111
     * @throws \Exception
112
     */
113 20
    protected function initialize(): void
114
    {
115 20
        unset($this->functions, $this->humanReadableName, $this->rewindable, $this->memoryUsageReport);
116
117 20
        $this->rewindable =
118 20
            new Rewindable(
119 20
                $this->iterationNumberGenerator,
120 20
                $this->iterations
121
            );
122 20
        $this->humanReadableName = null;
123 20
        $this->functions = [];
124 20
        $this->functionIndex = 1;
125 20
        $this->launched = false;
126 20
        $this->resetComment();
127 20
        $this->added = new SimpleCounter('added');
128 20
        $this->benchmarked = new SimpleCounter('benchmarked');
129 20
        $this->memoryUsageReport = MemoryUsage::report();
130 20
        $this->doneIterations = 0;
131 20
        $this->totalIterations = 0;
132 20
        $this->report = (new BenchmarkReport())->buildOn($this);
133 20
    }
134
135 20
    protected function resetComment(): void
136
    {
137 20
        $this->comment = null;
138 20
    }
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 4
    public function showProgressBy(
165
        callable $onStart = null,
166
        callable $onAdvance = null,
167
        callable $onFinish = null
168
    ): Benchmark {
169 4
        $this->onStart = $onStart;
170 4
        $this->onAdvance = $onAdvance;
171 4
        $this->onFinish = $onFinish;
172 4
        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
     * @return string
254
     * @throws \Exception
255
     */
256 4
    public function stat(): string
257
    {
258
        return
259 4
            sprintf(
260 4
                'Done in: %s%s%s',
261 4
                $this->getTimer()->elapsed(),
262 4
                PHP_EOL,
263 4
                (string)$this->memoryUsageReport
264
            );
265
    }
266
267
    /**
268
     * @return Benchmark
269
     */
270 1
    public function showReturns(): Benchmark
271
    {
272 1
        $this->setShowReturns(true);
273 1
        foreach ($this->functions as $function) {
274 1
            $function->setShowReturns($this->isShowReturns());
275
        }
276 1
        return $this;
277
    }
278
279
    /**
280
     * @param bool $showReturns
281
     */
282 1
    public function setShowReturns(bool $showReturns): void
283
    {
284 1
        $this->showReturns = $showReturns;
285 1
    }
286
287
    /** {@inheritdoc} */
288 15
    protected function meetConditions(): void
289
    {
290 15
        if ($this->isNotLaunched()) {
291 10
            $this->run();
292
        }
293 15
    }
294
295
    /**
296
     * @return bool
297
     */
298 15
    public function isNotLaunched(): bool
299
    {
300 15
        return !$this->isLaunched();
301
    }
302
303
    /**
304
     * Launch benchmarking
305
     */
306 17
    public function run(): self
307
    {
308 17
        $this->displayComment();
309 17
        $this->launched = true;
310 17
        if ($this->onStart) {
311 3
            ($this->onStart)();
312
        }
313 17
        $this->execute();
314 17
        if ($this->onFinish) {
315 4
            ($this->onFinish)();
316
        }
317 17
        $this->doneIterationsCombined += $this->doneIterations;
318 17
        return $this;
319
    }
320
321 17
    protected function displayComment(): void
322
    {
323 17
        if (!$this->silent && null !== $this->comment) {
324
            $this->showComment($this->comment);
325
            $this->resetComment();
326
        }
327 17
    }
328
329
    protected function showComment(string $comment = ''): void
330
    {
331
        echo $comment . PHP_EOL;
332
    }
333
334
    /**
335
     * Benchmarking
336
     */
337 17
    protected function execute(): void
338
    {
339
        /** @var  BenchmarkFunction $f */
340 17
        foreach ($this->functions as $f) {
341 16
            if (!$f->execute()) {
342 7
                $this->totalIterations -= $this->iterations;
343 7
                continue;
344
            }
345 16
            $this->advanceStep = (int)($this->totalIterations / $this->advanceSteps);
346 16
            $this->bench($f);
347 16
            $this->benchmarked->bump();
348
        }
349 17
    }
350
351
    /**
352
     * @param BenchmarkFunction $f
353
     */
354 16
    protected function bench(BenchmarkFunction $f): void
355
    {
356 16
        $timer = $f->getTimer();
357 16
        $function = $f->getCallable();
358 16
        $args = $f->getArgs();
359 16
        foreach ($this->rewindable as $iteration) {
360 16
            $start = $timer->current();
361
            /** @noinspection DisconnectedForeachInstructionInspection */
362 16
            $function(...$args);
363 16
            $stop = $timer->current();
364 16
            $timer->bounds($start, $stop, $iteration);
365
            /** @noinspection DisconnectedForeachInstructionInspection */
366 16
            $this->progress();
367
        }
368 16
    }
369
370 16
    protected function progress(): void
371
    {
372 16
        $this->doneIterations++;
373 16
        if ($this->onAdvance && 0 === $this->doneIterations % $this->advanceStep) {
374 4
            ($this->onAdvance)();
375
        }
376 16
    }
377
}
378