Completed
Push — develop ( eb616e...328010 )
by Alec
03:31
created

Benchmark::execute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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