Passed
Push — develop ( e4594a...3cf29a )
by Alec
02:43
created

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