Completed
Push — develop ( 5a5dff...7e0445 )
by Alec
07:58 queued 03:29
created

Benchmark::addFunction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 2

Importance

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