Completed
Push — develop ( ca99c7...b953bd )
by Alec
03:03
created

Benchmark::isNotLaunched()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 7
    public function __construct(?int $iterations = null)
58
    {
59 7
        $this->iterations = $this->refineIterations($iterations);
60
61
        $this->generatorFunction = function (int $iterations, int $i = 1): \Generator {
62 4
            while ($i <= $iterations) {
63 4
                yield $i++;
64
            }
65 4
        };
66
67 7
        $this->timer = new Timer();
68 7
        $this->initialize();
69 7
    }
70
71 7
    private function refineIterations(?int $iterations): int
72
    {
73 7
        $iterations = $iterations ?? self::MIN_ITERATIONS;
74 7
        $this->assertIterations($iterations);
75 7
        return $iterations;
76
    }
77
78
    /**
79
     * @param int $iterations
80
     */
81 7
    protected function assertIterations(int $iterations): void
82
    {
83 7
        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 7
    }
91
92
    /**
93
     * Resets Benchmark object clear
94
     * @throws \Exception
95
     */
96 7
    private function initialize(): void
97
    {
98 7
        unset($this->functions, $this->humanReadableName, $this->rewindable, $this->memoryUsageReport);
99
100 7
        $this->humanReadableName = null;
101 7
        $this->rewindable =
102 7
            new Rewindable(
103 7
                $this->generatorFunction,
104 7
                $this->iterations
105
            );
106 7
        $this->functions = [];
107 7
        $this->added = new SimpleCounter('added');
108 7
        $this->benchmarked = new SimpleCounter('benchmarked');
109 7
        $this->memoryUsageReport = MemoryUsage::report();
110 7
        $this->doneIterations = 0;
111 7
        $this->totalIterations = 0;
112 7
        $this->report = (new BenchmarkReport())->buildOn($this);
113 7
    }
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 5
    public function addFunction($func, ...$args): void
146
    {
147 5
        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 4
            new BenchmarkFunction(
158 4
                $func,
159 4
                $this->refineName($func, $name),
160 4
                $this->functionIndex++,
161
                $args,
162 4
                $this->comment,
163 4
                $this->humanReadableName
164
            );
165 4
        $function->setShowReturns($this->showReturns);
166 4
        $this->functions[$function->enumeratedName()] = $function;
167 4
        $this->humanReadableName = null;
168 4
        $this->comment = null;
169 4
        $this->added->bump();
170 4
        $this->totalIterations += $this->iterations;
171 4
    }
172
173
    /**
174
     * @param callable $func
175
     * @param string $name
176
     * @return string
177
     */
178 4
    private function refineName($func, $name): string
179
    {
180 4
        if ($func instanceof \Closure) {
181 4
            $name = 'λ';
182
        }
183 4
        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 3
    public function useName(string $name): self
201
    {
202 3
        $this->humanReadableName = $name;
203 3
        return $this;
204
    }
205
206
    /**
207
     * @return string
208
     * @throws \Exception
209
     */
210 3
    public function stat(): string
211
    {
212
        return
213 3
            sprintf(
214 3
                'Done in: %s%s%s',
215 3
                $this->getTimer()->elapsed(),
216 3
                PHP_EOL,
217 3
                (string)$this->memoryUsageReport
218
            );
219
    }
220
221 5
    protected function meetConditions(): void
222
    {
223 5
        if ($this->isNotLaunched()) {
224 3
            $this->run();
225
        }
226 5
    }
227
228
    /**
229
     * @return bool
230
     */
231 5
    public function isNotLaunched(): bool
232
    {
233 5
        return !$this->isLaunched();
234
    }
235
236
    /**
237
     * @return bool
238
     */
239 5
    public function isLaunched(): bool
240
    {
241 5
        return $this->launched;
242
    }
243
244
    /**
245
     * Launch benchmarking
246
     */
247 5
    public function run(): self
248
    {
249 5
        $this->launched = true;
250 5
        if ($this->onStart) {
251 2
            ($this->onStart)();
252
        }
253 5
        $this->execute();
254 5
        if ($this->onFinish) {
255 2
            ($this->onFinish)();
256
        }
257 5
        $this->doneIterationsCombined += $this->doneIterations;
258 5
        return $this;
259
    }
260
261
    /**
262
     * Benchmarking
263
     */
264 5
    private function execute(): void
265
    {
266
        /** @var  BenchmarkFunction $f */
267 5
        foreach ($this->functions as $f) {
268 4
            if (!$f->execute()) {
269 3
                $this->totalIterations -= $this->iterations;
270 3
                continue;
271
            }
272 4
            $this->advanceStep = (int)($this->totalIterations / $this->advanceSteps);
273 4
            $this->bench($f);
274 4
            $this->benchmarked->bump();
275
        }
276 5
    }
277
278
    /**
279
     * @param BenchmarkFunction $f
280
     */
281 4
    private function bench(BenchmarkFunction $f): void
282
    {
283 4
        $timer = $f->getTimer();
284 4
        $function = $f->getCallable();
285 4
        $args = $f->getArgs();
286 4
        foreach ($this->rewindable as $iteration) {
287 4
            $start = microtime(true);
288
            /** @noinspection DisconnectedForeachInstructionInspection */
289 4
            $function(...$args);
290 4
            $stop = microtime(true);
291 4
            $timer->bounds($start, $stop, $iteration);
292
            /** @noinspection DisconnectedForeachInstructionInspection */
293 4
            $this->progress();
294
        }
295 4
    }
296
297 4
    private function progress(): void
298
    {
299 4
        $this->doneIterations++;
300 4
        if ($this->onAdvance && 0 === $this->doneIterations % $this->advanceStep) {
301 2
            ($this->onAdvance)();
302
        }
303 4
    }
304
305
}
306