Completed
Push — master ( 9c3b37...e42026 )
by Alec
03:16
created

Benchmark::withComment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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