Completed
Push — master ( 73c14a...25d6ea )
by Alec
04:15
created

Benchmark::refineName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

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