Completed
Push — develop ( 4a055d...f8787b )
by Alec
06:39
created

Benchmark::terminalWidth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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