Benchmark   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Test Coverage

Coverage 7.89%

Importance

Changes 23
Bugs 2 Features 1
Metric Value
wmc 24
eloc 109
c 23
b 2
f 1
dl 0
loc 247
ccs 9
cts 114
cp 0.0789
rs 10

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A directBenchmark() 0 16 2
A add() 0 12 1
A withName() 0 8 1
A getRevs() 0 4 1
A addResult() 0 3 1
A indirectBenchmark() 0 14 2
A bench() 0 23 4
A withComment() 0 8 1
A warmUp() 0 7 2
A run() 0 38 4
A message() 0 4 2
A progress() 0 4 2
1
<?php declare(strict_types=1);
2
3
namespace AlecRabbit\Tools;
4
5
use AlecRabbit\Accessories\MemoryUsage;
6
use AlecRabbit\Accessories\MemoryUsage\MemoryUsageReport;
7
use AlecRabbit\Tools\Internal\BenchmarkFunction;
8
use AlecRabbit\Tools\Internal\BenchmarkOptions;
9
use AlecRabbit\Tools\Internal\BenchmarkResult;
10
use AlecRabbit\Tools\Internal\MeasurementsResults;
11
use AlecRabbit\Tools\Reports\BenchmarkReport;
12
use MathPHP\Exception\BadDataException;
13
use MathPHP\Exception\OutOfBoundsException;
14
use Symfony\Component\Console\Output\ConsoleOutput;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Webmozart\Assert\Assert;
17
use function AlecRabbit\Helpers\bounds;
18
19
class Benchmark
20
{
21
    /** @var null|string */
22
    protected $comment;
23
24
    /** @var null|string */
25
    protected $name;
26
27
    /** @var BenchmarkOptions */
28
    protected $options;
29
30
    /** @var BenchmarkFunction[] */
31
    protected $functions = [];
32
33
    /** @var int */
34
    protected $index = 0;
35
36
    /** @var OutputInterface */
37
    protected $output;
38
39
    /** @var int */
40
    protected $maxIterations;
41
42
    /** @var int */
43
    protected $progressThreshold;
44
45
    /** @var BenchmarkResult[] */
46
    protected $results;
47
48
    /** @var null|MemoryUsageReport */
49
    protected $memoryUsageReport;
50
51 17
    public function __construct(BenchmarkOptions $options = null, ?OutputInterface $output = null)
52
    {
53 17
        $this->options = $options ?? new BenchmarkOptions();
54 17
        $this->output = $output ?? new ConsoleOutput();
55 17
        $this->maxIterations = $this->options->getMaxIterations();
56 17
        $this->progressThreshold = $this->options->getProgressThreshold();
57
//        if (extension_loaded('Xdebug')) {
58
//            trigger_error('XDebug extension is loaded', E_USER_WARNING);
59
//        }
60 17
    }
61
62
    /**
63
     * @param string $comment
64
     * @return Benchmark
65
     */
66
    public function withComment(string $comment): self
67
    {
68
        Assert::notWhitespaceOnly(
69
            $comment,
70
            'Expected a non-whitespace comment string. Got: "' . $comment . '"'
71
        );
72
        $this->comment = $comment;
73
        return $this;
74
    }
75
76
    /**
77
     * @param string $name
78
     * @return Benchmark
79
     */
80
    public function withName(string $name): self
81
    {
82
        Assert::notWhitespaceOnly(
83
            $name,
84
            'Expected a non-whitespace function name string. Got: "' . $name . '"'
85
        );
86
        $this->name = $name;
87
        return $this;
88
    }
89
90
    /**
91
     * @param mixed $func
92
     * @param mixed ...$args
93
     */
94
    public function add($func, ...$args): void
95
    {
96
        $this->functions[] =
97
            new BenchmarkFunction(
98
                $func,
99
                $args,
100
                ++$this->index,
101
                $this->name,
102
                $this->comment
103
            );
104
        $this->name = null;
105
        $this->comment = null;
106
    }
107
108
    public function run(): BenchmarkReport
109
    {
110
        $this->memoryUsageReport = MemoryUsage::getReport();
111
        $this->message((string)$this->memoryUsageReport);
112
        $this->message('');
113
114
        $this->message('Benchmarking: ');
115
116
        foreach ($this->functions as $function) {
117
            $this->message(
118
                sprintf(
119
                    '%s',
120
                    mb_str_pad($function->getAssignedName(), 30)
121
                ),
122
                false
123
            );
124
            if (!$function->execute()) {
125
                $exception = $function->getException();
126
                if ($exception instanceof \Throwable) {
127
                    $this->message(
128
                        sprintf(
129
                            'Exception[%s]: %s ',
130
                            get_class($exception),
131
                            $exception->getMessage()
132
                        )
133
                    );
134
                }
135
                continue;
136
            }
137
            $result = $this->bench($function);
138
            $this->message(' ' . $result);
139
        }
140
        $this->message('');
141
        $this->memoryUsageReport = MemoryUsage::getReport()->diff($this->memoryUsageReport);
142
        $this->message((string)$this->memoryUsageReport);
143
        $this->message('');
144
145
        return (new BenchmarkReport())->setFunctions($this->functions);
146
    }
147
148
    /**
149
     * @param string $message
150
     * @param bool $newline
151
     */
152
    protected function message(string $message, $newline = true): void
153
    {
154
        if ($this->options->isCli()) {
155
            $this->output->write($message, $newline);
156
        }
157
    }
158
159
    /**
160
     * @param BenchmarkFunction $f
161
     * @return BenchmarkResult
162
     * @throws BadDataException
163
     * @throws OutOfBoundsException
164
     */
165
    protected function bench(BenchmarkFunction $f): BenchmarkResult
166
    {
167
        $r = [];
168
        $this->warmUp($f);
169
        $n = 0;
170
        while ($n++ <= 6) {
171
            $r[] = $this->indirectBenchmark(1000, $f);
172
        }
173
        $result = MeasurementsResults::createResult($r);
174
        $n = 0;
175
        while ($n <= $this->maxIterations) {
176
            try {
177
                $benchmarkResult = $this->directBenchmark($this->getRevs($n, 5), $f, $result);
178
                $r[] = $benchmarkResult;
179
            } catch (BadDataException $e) {
180
                // Result rejected
181
//                $this->message('Result rejected');
182
            }
183
            $n++;
184
        }
185
        $result = MeasurementsResults::createResult($r);
186
        $f->setResult($result);
187
        return $result;
188
    }
189
190
    /**
191
     * @param BenchmarkFunction $f
192
     * @param int $max
193
     */
194
    protected function warmUp(BenchmarkFunction $f, int $max = 3): void
195
    {
196
        $n = 0;
197
        while ($n++ <= $max) {
198
            $this->indirectBenchmark(1000, $f);
199
        }
200
        $this->progress();
201
    }
202
203
    protected function indirectBenchmark(int $i, BenchmarkFunction $f): BenchmarkResult
204
    {
205
        $function = $f->getCallable();
206
        $args = $f->getArgs();
207
208
        $revs = $i;
209
        $start = hrtime(true);
210
        while ($i-- > 0) {
211
            $function(...$args);
212
        }
213
        $stop = hrtime(true) - $start;
214
        $this->progress();
215
        return
216
            new BenchmarkResult($stop / $revs, 0, $revs);
217
    }
218
219
    protected function progress(?int $done = null): void
220
    {
221
        if (0 === ($done ?? 0) % $this->progressThreshold) {
222
            $this->message('.', false);
223
        }
224
    }
225
226
    /**
227
     * @param int $i
228
     * @param BenchmarkFunction $f
229
     * @param BenchmarkResult|null $previous
230
     * @return BenchmarkResult
231
     * @throws BadDataException
232
     * @throws OutOfBoundsException
233
     */
234
    protected function directBenchmark(int $i, BenchmarkFunction $f, ?BenchmarkResult $previous = null): BenchmarkResult
235
    {
236
        $function = $f->getCallable();
237
        $args = $f->getArgs();
238
        $measurements = [];
239
        $done = 0;
240
        while ($i > 0) {
241
            $start = hrtime(true);
242
            $function(...$args);
243
            $stop = hrtime(true);
244
            $measurements[] = $stop - $start;
245
            $done++;
246
            $i--;
247
            $this->progress($done);
248
        }
249
        return MeasurementsResults::createResult($measurements, $previous);
250
    }
251
252
    /**
253
     * @param int $n
254
     * @param int|null $shift
255
     * @return int
256
     */
257 16
    protected function getRevs(int $n, ?int $shift = null): int
258
    {
259 16
        $shift = $shift ?? 0;
260 16
        return (int)(10 ** bounds($n, 1, 5) + $shift);
261
    }
262
263
    protected function addResult(BenchmarkResult $result): void
264
    {
265
        $this->results[] = $result;
266
    }
267
}
268