Completed
Push — develop ( d2e190...d276d7 )
by Alec
10:43 queued 07:55
created

Benchmark::withComment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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