Passed
Push — develop ( b953bd...a0bdf0 )
by Alec
03:17
created

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