Completed
Push — develop ( 9d2605...6ca366 )
by Alec
02:51
created

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