Completed
Push — develop ( 500e55...0a2c3f )
by Alec
02:54
created

Benchmark::showReturns()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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