Passed
Push — develop ( aa265d...8b49d6 )
by Alec
14:04
created

Benchmark::initialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 1

Importance

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