• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devmarkusb / util / 26667680711

29 May 2026 11:36PM UTC coverage: 97.388% (-0.001%) from 97.389%
26667680711

push

github

MarkusB
fix, lint

6785 of 6967 relevant lines covered (97.39%)

126.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.33
/include/mb/ul/basiccodesupport/profiler.hpp
1
//! \file
2

3
#ifndef PROFILER_H_958X2G58SDJKFH23789HGXT34
4
#define PROFILER_H_958X2G58SDJKFH23789HGXT34
5

6
#include "assert.hpp"
7
#include "round.hpp"
8
#include "type/non_copyable.hpp"
9

10
#include "mb/ul/buildenv/config.hpp"
11
#include <algorithm>
12
#include <chrono>
13
#include <cmath>
14
#include <concepts>
15
#include <cstddef>
16
#include <cstdint>
17
#include <iomanip>
18
#include <iterator>
19
#include <limits>
20
#include <map>
21
#include <numeric>
22
#include <ranges>
23
#include <sstream>
24
#include <string>
25
#include <string_view>
26
#include <vector>
27

28
namespace mb::ul {
29
using ProfilerTimePoint = std::chrono::time_point<std::chrono::steady_clock>;
30

31
//! Returns the current monotonic time point for lightweight elapsed-time checks.
32
[[nodiscard]] inline ProfilerTimePoint profiler_now() {
2✔
33
    return std::chrono::steady_clock::now();
2✔
34
}
35

36
//! Returns the duration between two profiler time points.
37
[[nodiscard]] inline auto profiler_diff(ProfilerTimePoint start, ProfilerTimePoint end) {
1✔
38
    return end - start;
1✔
39
}
40

41
//! Monotonic clock used by PerformanceProfiler by default.
42
struct ProfilerSteadyClock {
43
    using Rep = double;
44
    using Period = std::ratio<1>;
45
    using Duration = std::chrono::duration<Rep, Period>;
46
    using TimePoint = std::chrono::time_point<ProfilerSteadyClock, Duration>;
47

48
    [[nodiscard]] static TimePoint now() noexcept {
49
        const auto elapsed = std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch());
50
        return TimePoint{Duration{elapsed.count()}};
51
    }
52
};
53

54
using ProfilerSystemClock = ProfilerSteadyClock;
55

56
//! Manual clock for deterministic PerformanceProfiler tests.
57
struct ManualProfilerClock {
58
    using Rep = double;
59
    using Period = std::ratio<1>;
60
    using Duration = std::chrono::duration<Rep, Period>;
61
    using TimePoint = std::chrono::time_point<ManualProfilerClock, Duration>;
62

63
    [[nodiscard]] static TimePoint now() noexcept {
29✔
64
        return TimePoint{Duration{elapsed_seconds()}};
29✔
65
    }
66

67
    //! Advances the manual clock by \p seconds.
68
    static void advance(double seconds) {
10✔
69
        elapsed_seconds() += seconds;
10✔
70
    }
10✔
71

72
    //! Resets the manual clock to zero.
73
    static void reset() noexcept {
2✔
74
        elapsed_seconds() = 0.0;
2✔
75
    }
2✔
76

77
private:
78
    [[nodiscard]] static double& elapsed_seconds() {
41✔
79
        static double elapsed{};
80
        return elapsed;
41✔
81
    }
82
};
83

84
//! Requirements for injectable PerformanceProfiler clocks.
85
template <typename Clock>
86
concept ProfilerClock = requires {
87
    typename Clock::Duration;
88
    typename Clock::TimePoint;
89
    { Clock::now() } noexcept -> std::same_as<typename Clock::TimePoint>;
90
};
91

92
//! Thread-local-scoped hierarchical scope profiler. Cf. unit tests.
93
template <ProfilerClock Clock = ProfilerSteadyClock>
94
class PerformanceProfiler : private NonCopyable {
95
public:
96
    using TimeValStorageRep = double;
97
    using SecondsDbl = TimeValStorageRep;
98
    using NestingLevel = unsigned int;
99

100
    explicit PerformanceProfiler(std::string_view new_item_name);
101

102
    //! \param nesting_level is just for dump visualization
103
    explicit PerformanceProfiler(std::string_view new_item_name, NestingLevel nesting_level);
104
    ~PerformanceProfiler() noexcept;
105
    [[nodiscard]] SecondsDbl elapsed_current_item() const;
106

107
    //! New item on same hierarchy/nesting level.
108
    void start_new_item(std::string_view new_item_name);
109

110
    //! Stops the current item without starting a replacement.
111
    void stop_item();
112

113
    enum class DumpFormat : uint8_t {
114
        string_only,
115
        string_and_structure,
116
    };
117

118
    //! Also fills dumped_data() if fmt is DumpFormat::string_and_structure.
119
    template <DumpFormat fmt = DumpFormat::string_only>
120
    [[nodiscard]] static std::string dump_all_items();
121

122
    static void reset();
123

124
    //! Formats \p seconds for human-readable profiler output.
125
    [[nodiscard]] static std::string to_formatted_string(SecondsDbl seconds);
126

127
    //! Only for testing.
128
    struct DumpDataset {
129
        std::string item_name;
130
        size_t count{};
131
        SecondsDbl total{};
132
        SecondsDbl average{};
133
        SecondsDbl mean{};
134
        SecondsDbl std_dev{};
135
    };
136

137
    //! Filled with structured data (e.g. for testing) if dump_all_items() was called with
138
    //! DumpFormat::string_and_structure.
139
    [[nodiscard]] static std::vector<DumpDataset>& dumped_data() {
13✔
140
        static std::vector<DumpDataset> data;
13✔
141
        return data;
13✔
142
    }
143

144
private:
145
    struct KeyData;
146
    using UniqueItemStartNr = uint64_t;
147

148
    struct ItemData {
149
        TimeValStorageRep time_val{};
150
        NestingLevel nesting_level{};
151
        UniqueItemStartNr start_nr{};
152
    };
153

154
    using ItemNameAsKey = std::string;
155
    using Items = std::multimap<ItemNameAsKey, ItemData>;
156
    using ChronoTimepoint = typename Clock::TimePoint;
157

158
    ChronoTimepoint start_time_{};
159
    std::string item_name_;
160
    NestingLevel nesting_level_{};
161
    UniqueItemStartNr item_start_nr_{};
162

163
    void start_current_item();
164
    void stop_current_item();
165

166
    [[nodiscard]] static UniqueItemStartNr& unique_item_start_nr() {
16✔
167
        static UniqueItemStartNr next_item_start_nr{};
168
        return next_item_start_nr;
16✔
169
    }
170

171
    [[nodiscard]] static Items& items();
172

173
    struct MatchKey {
174
        explicit MatchKey(std::string_view key)
9✔
175
            : key_{key} {
18✔
176
        }
9✔
177

178
        [[nodiscard]] bool operator()(const Items::value_type& rhs) const {
117✔
179
            return key_ == rhs.first;
117✔
180
        }
181

182
    private:
183
        ItemNameAsKey key_;
184
    };
185

186
    struct AccumKey {
187
        explicit AccumKey(std::string_view key)
9✔
188
            : key_{key} {
18✔
189
        }
9✔
190

191
        [[nodiscard]] TimeValStorageRep operator()(TimeValStorageRep value, const Items::value_type& rhs) const {
117✔
192
            if (key_ == rhs.first) {
117✔
193
                return rhs.second.time_val + value;
13✔
194
            }
195
            return value;
104✔
196
        }
197

198
    private:
199
        ItemNameAsKey key_;
200
    };
201

202
    struct KeyData {
203
        NestingLevel nesting_level{};
204
        UniqueItemStartNr start_nr{};
205
    };
206

207
    struct KeyEntryMaker {
208
        [[nodiscard]] std::pair<ItemNameAsKey, KeyData> operator()(const Items::value_type& item) const {
13✔
209
            return {
210
                item.first,
13✔
211
                KeyData{item.second.nesting_level, item.second.start_nr},
13✔
212
            };
13✔
213
        }
214
    };
215

216
    struct StartNrLess {
217
        template <typename KeyNameAndData>
218
        [[nodiscard]] bool operator()(const KeyNameAndData& left, const KeyNameAndData& right) const {
16✔
219
            return left.second.start_nr < right.second.start_nr;
16✔
220
        }
221
    };
222
};
223

224
template <ProfilerClock Clock>
225
void PerformanceProfiler<Clock>::start_current_item() {
13✔
226
    item_start_nr_ = unique_item_start_nr()++;
13✔
227
    start_time_ = Clock::now();
13✔
228
}
13✔
229

230
template <ProfilerClock Clock>
231
void PerformanceProfiler<Clock>::stop_current_item() {
14✔
232
    ItemData item_data{elapsed_current_item(), nesting_level_, item_start_nr_};
14✔
233
    const auto item_it = items().find(item_name_);
14✔
234
    if (item_it != items().end()) {
14✔
235
        // if on the same nesting level, we want to keep the order of a single run through this level;
236
        // we don't know which representative will be picked later to generate a data record
237
        if (nesting_level_ == item_it->second.nesting_level) {
4✔
238
            item_data.start_nr = item_it->second.start_nr;
4✔
239
        }
240
    }
241
    items().emplace(item_name_, item_data);
14✔
242
}
14✔
243

244
template <ProfilerClock Clock>
245
PerformanceProfiler<Clock>::PerformanceProfiler(std::string_view new_item_name)
1✔
246
    : PerformanceProfiler(new_item_name, NestingLevel{}) {
1✔
247
}
1✔
248

249
template <ProfilerClock Clock>
250
PerformanceProfiler<Clock>::PerformanceProfiler(std::string_view new_item_name, NestingLevel nesting_level)
8✔
251
    : item_name_{new_item_name}
16✔
252
    , nesting_level_{nesting_level} {
8✔
253
    start_current_item();
8✔
254
}
8✔
255

256
template <ProfilerClock Clock>
257
PerformanceProfiler<Clock>::SecondsDbl PerformanceProfiler<Clock>::elapsed_current_item() const {
16✔
258
    const auto now{Clock::now()};
16✔
259
    const auto elapsed{now - start_time_};
16✔
260
    return elapsed.count();
32✔
261
}
262

263
template <ProfilerClock Clock>
264
PerformanceProfiler<Clock>::~PerformanceProfiler() noexcept {
8✔
265
    try {
266
        stop_current_item();
8✔
267
    } catch (...) {
×
268
    }
269
}
8✔
270

271
template <ProfilerClock Clock>
272
void PerformanceProfiler<Clock>::start_new_item(std::string_view new_item_name) {
5✔
273
    stop_current_item();
5✔
274
    item_name_ = new_item_name;
5✔
275
    start_current_item();
5✔
276
}
5✔
277

278
template <ProfilerClock Clock>
279
void PerformanceProfiler<Clock>::stop_item() {
1✔
280
    stop_current_item();
1✔
281
}
1✔
282

283
namespace impl_profiler_dump {
284
[[nodiscard]] inline std::string item_name_with_nesting(
9✔
285
    std::string_view item_name, unsigned int nesting_level, size_t column_width_huge) {
286
    std::ostringstream item_name_stream{};
9✔
287
    constexpr unsigned int nesting_levels_symbolized_by_spaces{20U};
9✔
288
    for (
9✔
289
        unsigned int nesting_level_index{1U};
9✔
290
        nesting_level_index <= nesting_level && nesting_level_index <= nesting_levels_symbolized_by_spaces;
18✔
291
        ++nesting_level_index) {
292
        item_name_stream << ' ';
9✔
293
    }
294
    item_name_stream << item_name;
9✔
295
    auto item_name_with_nesting_level{item_name_stream.str()};
9✔
296
    if (item_name_with_nesting_level.length() >= column_width_huge) {
9✔
297
        item_name_with_nesting_level.resize(column_width_huge - 1U);
×
298
    }
299
    return item_name_with_nesting_level;
9✔
300
}
9✔
301

302
[[nodiscard]] inline double variance(const std::vector<double>& sorted_items, double mean_seconds) {
1✔
303
    struct SquaredDiffFromMean {
304
        double mean_seconds{};
305

306
        explicit SquaredDiffFromMean(double mean)
1✔
307
            : mean_seconds{mean} {
1✔
308
        }
1✔
309

310
        [[nodiscard]] double operator()(double sum, double item_seconds) const {
5✔
311
            const auto delta{item_seconds - mean_seconds};
5✔
312
            return sum + (delta * delta);
5✔
313
        }
314
    };
315

316
    return std::ranges::fold_left(sorted_items, 0.0, SquaredDiffFromMean{mean_seconds});
1✔
317
}
318
} // namespace impl_profiler_dump
319

320
// NOLINTBEGIN
321
template <ProfilerClock Clock>
322
template <typename PerformanceProfiler<Clock>::DumpFormat fmt>
323
std::string PerformanceProfiler<Clock>::dump_all_items() {
2✔
324
    if constexpr (fmt != DumpFormat::string_only) {
325
        dumped_data().clear();
2✔
326
    }
327
    std::ostringstream output{};
2✔
328
    if (items().empty()) {
2✔
329
        output << "No performance measurement data." << std::endl;
1✔
330
        return output.str();
1✔
331
    }
332

333
    using KeySetUnsorted = std::map<ItemNameAsKey, KeyData>;
334
    KeySetUnsorted keys_unsorted{};
1✔
335
    std::transform(
3✔
336
        items().begin(), items().end(), std::inserter(keys_unsorted, keys_unsorted.begin()), KeyEntryMaker{});
2✔
337
    using KeyNameAndData = std::pair<ItemNameAsKey, KeyData>;
338
    using KeySet = std::vector<KeyNameAndData>;
339
    KeySet keys{keys_unsorted.begin(), keys_unsorted.end()};
2✔
340
    std::sort(keys.begin(), keys.end(), StartNrLess{});
1✔
341

342
    constexpr size_t column_width{10U};
1✔
343
    constexpr size_t column_width_huge{29U};
1✔
344

345
    output << std::left;
1✔
346
    output << std::setfill('-') << std::setw(static_cast<int>(column_width_huge + column_width * 5U)) << '-'
1✔
347
           << std::endl;
1✔
348
    output << std::setfill(' ');
1✔
349
    output << std::setw(static_cast<int>(column_width_huge)) << std::setprecision(static_cast<int>(column_width_huge))
350
           << "Item" << std::setw(static_cast<int>(column_width)) << std::setprecision(static_cast<int>(column_width))
351
           << "Count" << std::setw(static_cast<int>(column_width)) << std::setprecision(static_cast<int>(column_width))
352
           << "Total" << std::setw(static_cast<int>(column_width)) << std::setprecision(static_cast<int>(column_width))
353
           << "Average" << std::setw(static_cast<int>(column_width))
354
           << std::setprecision(static_cast<int>(column_width)) << "Mean" << std::setw(static_cast<int>(column_width))
355
           << std::setprecision(static_cast<int>(column_width)) << "StdDev" << std::endl;
1✔
356
    output << std::setfill('-') << std::setw(static_cast<int>(column_width_huge + column_width * 5U)) << '-'
1✔
357
           << std::endl;
1✔
358
    output << std::setfill(' ');
1✔
359

360
    for (const auto& [item_name, key_data] : keys) {
29✔
361
        const auto total_seconds =
362
            std::accumulate(items().begin(), items().end(), TimeValStorageRep{}, AccumKey{item_name});
9✔
363
        const auto count = std::count_if(items().begin(), items().end(), MatchKey{item_name});
9✔
364
        UL_ASSERT(count >= 0);
365
        const auto average_seconds = count != 0 ? total_seconds / static_cast<TimeValStorageRep>(count)
9✔
366
                                                : std::numeric_limits<TimeValStorageRep>::infinity();
×
367

368
        std::vector<TimeValStorageRep> sorted_items{};
9✔
369
        for (const auto& [current_item_name, current_item_data] : items()) {
126✔
370
            if (item_name == current_item_name) {
117✔
371
                sorted_items.push_back(current_item_data.time_val);
13✔
372
            }
373
        }
374
        std::sort(sorted_items.begin(), sorted_items.end());
9✔
375
        const auto mid = static_cast<size_t>(std::floor(static_cast<double>(count) / 2.0));
9✔
376
        const auto mean_seconds =
8✔
377
            (count > 1 && count % 2) ? (sorted_items[mid] + sorted_items[mid + 1U]) / 2.0 : sorted_items[mid];
9✔
378

379
        const auto variance_seconds = count > 1 ? impl_profiler_dump::variance(sorted_items, mean_seconds) : 0.0;
9✔
380
        const auto stddev_seconds = count > 1 ? std::sqrt(variance_seconds / (static_cast<double>(count) - 1.0)) : 0.0;
9✔
381

382
        const auto item_name_with_nesting_level =
9✔
383
            impl_profiler_dump::item_name_with_nesting(item_name, key_data.nesting_level, column_width_huge);
9✔
384
        output << std::setw(static_cast<int>(column_width_huge))
385
               << std::setprecision(static_cast<int>(column_width_huge)) << item_name_with_nesting_level
386
               << std::setw(static_cast<int>(column_width)) << std::setprecision(static_cast<int>(column_width))
9✔
387
               << count << std::setw(static_cast<int>(column_width))
9✔
388
               << std::setprecision(static_cast<int>(column_width)) << to_formatted_string(total_seconds)
9✔
389
               << std::setw(static_cast<int>(column_width)) << std::setprecision(static_cast<int>(column_width))
390
               << to_formatted_string(average_seconds) << std::setw(static_cast<int>(column_width))
18✔
391
               << std::setprecision(static_cast<int>(column_width)) << to_formatted_string(mean_seconds)
18✔
392
               << std::setw(static_cast<int>(column_width)) << std::setprecision(static_cast<int>(column_width))
393
               << to_formatted_string(stddev_seconds) << std::endl;
36✔
394
        if constexpr (fmt != DumpFormat::string_only) {
395
            dumped_data().push_back(
27✔
396
                {item_name_with_nesting_level,
397
                 static_cast<size_t>(count),
9✔
398
                 total_seconds,
399
                 average_seconds,
400
                 mean_seconds,
401
                 stddev_seconds});
402
        }
403
    }
404
    output << std::setfill('-') << std::setw(static_cast<int>(column_width_huge + column_width * 5U)) << '-'
1✔
405
           << std::endl;
1✔
406
    output << std::setfill(' ');
1✔
407
    return output.str();
1✔
408
}
20✔
409

410
// NOLINTEND
411

412
template <ProfilerClock Clock>
413
std::string PerformanceProfiler<Clock>::to_formatted_string(SecondsDbl seconds) {
62✔
414
    std::ostringstream output{};
62✔
415
    auto absolute_seconds{seconds};
62✔
416
    if (absolute_seconds < 0.0) {
62✔
417
        absolute_seconds = -absolute_seconds;
1✔
418
        output << '-';
1✔
419
    }
420
    // NOLINTBEGIN
421
    if (absolute_seconds == 0.0 || absolute_seconds < 0.000000000099995) { // 0.0000000001
62✔
422
        output << std::setprecision(2) << std::fixed << math::round(absolute_seconds * 1000000000000.0, 2) << " ps";
13✔
423
    } else if (absolute_seconds < 0.000000099995) { // 0.0000001
49✔
424
        output << std::setprecision(2) << std::fixed << math::round(absolute_seconds * 1000000000.0, 2) << " ns";
3✔
425
    } else if (absolute_seconds < 0.000099995) { // 0.0001
46✔
426
        output << std::setprecision(2) << std::fixed << math::round(absolute_seconds * 1000000.0, 2) << " "
3✔
427
               << "\xC2\xB5"
428
               << "s";
3✔
429
    } else if (absolute_seconds < 0.099995) { // 0.1
43✔
430
        output << std::setprecision(2) << std::fixed << math::round(absolute_seconds * 1000.0, 2) << " ms";
3✔
431
    } else if (absolute_seconds < 59.995) { // 60.0
40✔
432
        output << std::setprecision(2) << std::fixed << math::round(absolute_seconds, 2) << " s";
33✔
433
    } else if (absolute_seconds < 3600.0) { // 3600.0
7✔
434
        output << std::setw(2) << std::setfill('0') << std::floor(absolute_seconds / 60.0) << ':' << std::setw(5)
3✔
435
               << std::fixed << std::setprecision(2) << std::setfill('0') << std::fmod(absolute_seconds, 60.0);
3✔
436
    } else if (absolute_seconds < 359999.0) {
4✔
437
        output << std::setw(2) << std::setfill('0') << std::floor(absolute_seconds / 3600.0) << ':' << std::setw(2)
3✔
438
               << std::setfill('0') << std::floor(std::fmod(absolute_seconds, 3600.0) / 60.0) << ':' << std::setw(2)
3✔
439
               << std::setfill('0') << std::setprecision(0) << std::fixed
3✔
440
               << std::floor(std::fmod(absolute_seconds, 60.0));
3✔
441
    } else {
442
        output << ">= 100 h";
1✔
443
    }
444
    // NOLINTEND
445
    return output.str();
124✔
446
}
62✔
447

448
template <ProfilerClock Clock>
449
void PerformanceProfiler<Clock>::reset() {
3✔
450
    items().clear();
3✔
451
    unique_item_start_nr() = UniqueItemStartNr{};
3✔
452
}
3✔
453

454
template <ProfilerClock Clock>
455
typename PerformanceProfiler<Clock>::Items& PerformanceProfiler<Clock>::items() {
94✔
456
    static Items instance{};
94✔
457
    return instance;
94✔
458
}
459

460
//! Default monotonic-clock profiler; use for static calls such as dump_all_items().
461
using DefaultPerformanceProfiler = PerformanceProfiler<>;
462
} // namespace mb::ul
463

464
UL_HEADER_END
465

466
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc