libpromeki 1.0.0-alpha
PROfessional MEdia toolKIt
 
Loading...
Searching...
No Matches
benchmarkrunner.h
Go to the documentation of this file.
1
8#pragma once
9
10
11#include <promeki/config.h>
12#if PROMEKI_ENABLE_CORE
13#include <cstdint>
14#include <functional>
15#include <promeki/function.h>
16#include <promeki/namespace.h>
17#include <promeki/string.h>
18#include <promeki/list.h>
19#include <promeki/map.h>
20#include <promeki/error.h>
23#include <promeki/util.h>
24
25PROMEKI_NAMESPACE_BEGIN
26
27class JsonObject;
28class BenchmarkRunner;
29
67class BenchmarkState {
68 public:
76 class Iterator {
77 public:
78 Iterator(BenchmarkState *state, uint64_t pos) : _state(state), _pos(pos) {}
79
80 uint64_t operator*() const { return _pos; }
81
82 Iterator &operator++() {
83 ++_pos;
84 if (_state != nullptr && _pos >= _state->_iterations) {
85 _state->finishTiming();
86 }
87 return *this;
88 }
89
90 bool operator!=(const Iterator &other) const { return _pos != other._pos; }
91
92 private:
93 BenchmarkState *_state;
94 uint64_t _pos;
95 };
96
98 explicit BenchmarkState(uint64_t iterations) : _iterations(iterations) {}
99
101 uint64_t iterations() const { return _iterations; }
102
107 Iterator begin() {
108 ensureTimerStarted();
109 return Iterator(this, 0);
110 }
111
113 Iterator end() { return Iterator(this, _iterations); }
114
125 bool keepRunning() {
126 if (_counter == 0) ensureTimerStarted();
127 if (_counter >= _iterations) {
128 finishTiming();
129 return false;
130 }
131 _counter++;
132 return true;
133 }
134
142 void pauseTiming() {
143 if (!_timerStarted || _paused) return;
144 _paused = true;
145 _pauseStartNs = _timer.elapsedNs();
146 return;
147 }
148
150 void resumeTiming() {
151 if (!_paused) return;
152 _paused = false;
153 _pausedNs += _timer.elapsedNs() - _pauseStartNs;
154 return;
155 }
156
166 void setItemsProcessed(uint64_t n) { _itemsProcessed = n; }
167
175 void setBytesProcessed(uint64_t n) { _bytesProcessed = n; }
176
181 void setLabel(const String &label) { _label = label; }
182
194 void setCounter(const String &key, double value) {
195 _counters.insert(key, value);
196 return;
197 }
198
200 const String &label() const { return _label; }
201
203 uint64_t itemsProcessed() const { return _itemsProcessed; }
204
206 uint64_t bytesProcessed() const { return _bytesProcessed; }
207
209 const Map<String, double> &counters() const { return _counters; }
210
215 int64_t effectiveNs() const { return _effectiveNs; }
216
229 int64_t wallNs() const { return _wallNs; }
230
237 bool completed() const { return _completed; }
238
239 private:
240 friend class BenchmarkRunner;
241
242 void ensureTimerStarted() {
243 if (_timerStarted) return;
244 _timerStarted = true;
245 _timer.start();
246 }
247
248 void finishTiming() {
249 if (_completed) return;
250 _completed = true;
251 if (_paused) {
252 _pausedNs += _timer.elapsedNs() - _pauseStartNs;
253 _paused = false;
254 }
255 _wallNs = _timer.elapsedNs();
256 _effectiveNs = _wallNs - _pausedNs;
257 if (_effectiveNs < 0) _effectiveNs = 0;
258 return;
259 }
260
262 void captureIfUnfinished() {
263 // If the case used range-for, iteration exits without
264 // calling keepRunning(), so finishTiming() hasn't run.
265 if (_completed) return;
266 if (_timerStarted) {
267 finishTiming();
268 } else {
269 // Case never touched the state at all — leave
270 // _completed false so the runner can flag it.
271 }
272 }
273
274 uint64_t _iterations = 0;
275 uint64_t _counter = 0;
276 bool _timerStarted = false;
277 bool _paused = false;
278 bool _completed = false;
279 ElapsedTimer _timer;
280 int64_t _pauseStartNs = 0;
281 int64_t _pausedNs = 0;
282 int64_t _wallNs = 0;
283 int64_t _effectiveNs = 0;
284 uint64_t _itemsProcessed = 0;
285 uint64_t _bytesProcessed = 0;
286 String _label;
287 Map<String, double> _counters;
288};
289
299class BenchmarkResult {
300 public:
302 String suite;
304 String name;
306 String label;
308 String description;
310 uint64_t iterations = 0;
312 uint64_t repeats = 0;
314 double avgNsPerIter = 0.0;
316 double minNsPerIter = 0.0;
318 double maxNsPerIter = 0.0;
320 double stddevNsPerIter = 0.0;
322 double itemsPerSecond = 0.0;
324 double bytesPerSecond = 0.0;
326 Map<String, double> custom;
328 bool succeeded = true;
330 String errorMessage;
331
333 JsonObject toJson() const;
334
341 static BenchmarkResult fromJson(const JsonObject &obj, Error *err = nullptr);
342};
343
353class BenchmarkCase {
354 public:
356 using Function = promeki::Function<void(BenchmarkState &)>;
357
365 BenchmarkCase(const String &suite, const String &name, const String &description, Function fn)
366 : _suite(suite), _name(name), _description(description), _fn(std::move(fn)) {}
367
369 const String &suite() const { return _suite; }
370
372 const String &name() const { return _name; }
373
375 const String &description() const { return _description; }
376
378 String fullName() const { return _suite + "." + _name; }
379
381 void invoke(BenchmarkState &state) const { _fn(state); }
382
383 private:
384 String _suite;
385 String _name;
386 String _description;
387 Function _fn;
388};
389
415class BenchmarkRunner {
416 public:
427 static int registerCase(const BenchmarkCase &theCase);
428
430 static const List<BenchmarkCase> &registeredCases();
431
446 static String formatRegisteredCases();
447
449 BenchmarkRunner();
450
455 void setMinTimeMs(unsigned int ms) { _minTimeMs = ms; }
456
461 void setWarmupMs(unsigned int ms) { _warmupMs = ms; }
462
467 void setMinIterations(uint64_t n) { _minIterations = n; }
468
473 void setMaxIterations(uint64_t n) { _maxIterations = n; }
474
483 void setRepeats(unsigned int n) { _repeats = n == 0 ? 1 : n; }
484
494 void setFilter(const String &pattern) { _filterPattern = pattern; }
495
500 void setVerbose(bool verbose) { _verbose = verbose; }
501
503 unsigned int minTimeMs() const { return _minTimeMs; }
504
506 unsigned int warmupMs() const { return _warmupMs; }
507
509 uint64_t minIterations() const { return _minIterations; }
510
512 uint64_t maxIterations() const { return _maxIterations; }
513
515 unsigned int repeats() const { return _repeats; }
516
518 const String &filter() const { return _filterPattern; }
519
521 const List<BenchmarkResult> &results() const { return _results; }
522
532 int filteredCaseCount() const;
533
547 int64_t estimatedDurationMs() const;
548
553 Error runAll();
554
560 Error runCaseByName(const String &fullName);
561
567 BenchmarkResult runCase(const BenchmarkCase &theCase);
568
570 void clearResults() { _results.clear(); }
571
577 Error writeJson(const String &path) const;
578
587 JsonObject toJson() const;
588
595 static List<BenchmarkResult> loadBaseline(const String &path, Error *err = nullptr);
596
604 String formatTable() const;
605
610 String formatComparison(const List<BenchmarkResult> &baseline) const;
611
612 private:
613 BenchmarkResult measureCase(const BenchmarkCase &theCase);
614 uint64_t calibrateIterations(const BenchmarkCase &theCase);
615 void runOne(const BenchmarkCase &theCase, uint64_t iterations, BenchmarkState &state);
617 bool matchesFilter(const BenchmarkCase &theCase) const;
618
619 unsigned int _minTimeMs = 500;
620 unsigned int _warmupMs = 100;
621 uint64_t _minIterations = 1;
622 uint64_t _maxIterations = 1000000000ULL;
623 unsigned int _repeats = 1;
624 String _filterPattern;
625 bool _verbose = false;
626 // Column width used for the "running" progress line so
627 // each case's result lines up in the same place on
628 // screen; computed by runAll() from the filtered case
629 // set and read by measureCase() when printing the case
630 // name. 0 means "no padding" — measureCase falls back
631 // to plain width when called outside a runAll() sweep.
632 size_t _progressNameWidth = 0;
633 List<BenchmarkResult> _results;
634};
635
655#define PROMEKI_REGISTER_BENCHMARK(Suite, Name, Description, Fn) \
656 [[maybe_unused]] static int PROMEKI_CONCAT(__promeki_benchmark_, PROMEKI_UNIQUE_ID) = \
657 promeki::BenchmarkRunner::registerCase(promeki::BenchmarkCase((Suite), (Name), (Description), (Fn)));
658
659PROMEKI_NAMESPACE_END
660
661#endif // PROMEKI_ENABLE_CORE