include/DetourModKit/detail/profile_ring.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_DETAIL_PROFILE_RING_HPP | ||
| 2 | #define DETOURMODKIT_DETAIL_PROFILE_RING_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file profile_ring.hpp | ||
| 6 | * @brief The sample slot, the saturating tick conversion, and the ticket publication protocol behind @ref | ||
| 7 | * DetourModKit::Profiler. | ||
| 8 | * | ||
| 9 | * @details Separated from profiler.hpp so the publication protocol can be driven directly at a small capacity: the | ||
| 10 | * singleton's fixed 65536-slot ring cannot be stepped through a slot-reuse collision deterministically. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include <atomic> | ||
| 14 | #include <cstddef> | ||
| 15 | #include <cstdint> | ||
| 16 | #include <memory> | ||
| 17 | #include <new> | ||
| 18 | |||
| 19 | namespace DetourModKit::detail | ||
| 20 | { | ||
| 21 | /** | ||
| 22 | * @brief Computes `remainder * multiplier / divisor` exactly without a wide integer type. | ||
| 23 | * @param remainder Numerator, which must be smaller than @p divisor. | ||
| 24 | * @param multiplier Scale factor. | ||
| 25 | * @param divisor Denominator; zero yields 0. | ||
| 26 | * @return The truncated quotient. | ||
| 27 | * @pre `remainder < divisor`. The reduction step computes `divisor - remainder`, so a larger numerator wraps and | ||
| 28 | * the result is meaningless. Callers pass a modulus result, which satisfies this by construction. | ||
| 29 | */ | ||
| 30 | [[nodiscard]] inline constexpr std::uint64_t | ||
| 31 | 13 | multiply_fraction(std::uint64_t remainder, std::uint64_t multiplier, std::uint64_t divisor) noexcept | |
| 32 | { | ||
| 33 |
6/6✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 5 taken 2 times.
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 9 times.
|
13 | if (remainder == 0 || multiplier == 0 || divisor == 0) |
| 34 | { | ||
| 35 | 4 | return 0; | |
| 36 | } | ||
| 37 | |||
| 38 | 9 | std::uint64_t quotient = 0; | |
| 39 | 9 | std::uint64_t reduced = 0; | |
| 40 | |||
| 41 | 9 | std::uint64_t bit = std::uint64_t{1} << 63; | |
| 42 |
2/2✓ Branch 8 → 7 taken 447 times.
✓ Branch 8 → 9 taken 9 times.
|
456 | while ((bit & multiplier) == 0) |
| 43 | { | ||
| 44 | 447 | bit >>= 1; | |
| 45 | } | ||
| 46 | |||
| 47 |
2/2✓ Branch 19 → 10 taken 129 times.
✓ Branch 19 → 20 taken 9 times.
|
138 | for (; bit != 0; bit >>= 1) |
| 48 | { | ||
| 49 | 129 | quotient *= 2; | |
| 50 |
2/2✓ Branch 10 → 11 taken 85 times.
✓ Branch 10 → 12 taken 44 times.
|
129 | if (reduced >= divisor - reduced) |
| 51 | { | ||
| 52 | 85 | reduced -= divisor - reduced; | |
| 53 | 85 | ++quotient; | |
| 54 | } | ||
| 55 | else | ||
| 56 | { | ||
| 57 | 44 | reduced += reduced; | |
| 58 | } | ||
| 59 | |||
| 60 |
2/2✓ Branch 13 → 14 taken 83 times.
✓ Branch 13 → 15 taken 46 times.
|
129 | if ((bit & multiplier) == 0) |
| 61 | { | ||
| 62 | 83 | continue; | |
| 63 | } | ||
| 64 |
2/2✓ Branch 15 → 16 taken 26 times.
✓ Branch 15 → 17 taken 20 times.
|
46 | if (reduced >= divisor - remainder) |
| 65 | { | ||
| 66 | 26 | reduced -= divisor - remainder; | |
| 67 | 26 | ++quotient; | |
| 68 | } | ||
| 69 | else | ||
| 70 | { | ||
| 71 | 20 | reduced += remainder; | |
| 72 | } | ||
| 73 | } | ||
| 74 | 9 | return quotient; | |
| 75 | } | ||
| 76 | |||
| 77 | /** | ||
| 78 | * @struct ProfileSample | ||
| 79 | * @brief One ring slot: a committed timing sample plus the ticket word that publishes it. | ||
| 80 | */ | ||
| 81 | struct ProfileSample | ||
| 82 | { | ||
| 83 | /** | ||
| 84 | * @brief Publication word: `((ticket + 1) << 1) | busy`, where `ticket` is the ring write position that owns | ||
| 85 | * the slot and zero means the slot has never been committed. | ||
| 86 | * @details Odd means a write is in flight and readers must skip the slot. Offset encoding keeps the first | ||
| 87 | * committed ticket distinct from the zero-initialized state. Because the encoded ticket increases | ||
| 88 | * across every reuse of the slot, a reader that sees the same word before and after copying the | ||
| 89 | * payload has observed one committed sample. | ||
| 90 | */ | ||
| 91 | std::atomic<std::uint64_t> state{0}; | ||
| 92 | /** | ||
| 93 | * @brief Non-owning pointer to the sample name. | ||
| 94 | * @note Must outlive the process; the exporter reads it asynchronously. A null name marks a slot that has never | ||
| 95 | * been committed. | ||
| 96 | */ | ||
| 97 | const char *name{nullptr}; | ||
| 98 | /// QPC tick count at scope entry. | ||
| 99 | std::int64_t start_ticks{0}; | ||
| 100 | /// Duration in microseconds, saturated at UINT32_MAX (~71 minutes). | ||
| 101 | std::uint32_t duration_us{0}; | ||
| 102 | /// Win32 thread ID of the recording thread. | ||
| 103 | std::uint32_t thread_id{0}; | ||
| 104 | /** | ||
| 105 | * @brief Byte count of @ref name, published with it. | ||
| 106 | * @note The extent travels with the pointer, so the exporter never scans for a terminator. A source array | ||
| 107 | * with no null still exports exactly its own bytes. | ||
| 108 | */ | ||
| 109 | std::uint32_t name_length{0}; | ||
| 110 | |||
| 111 | 2555922 | ProfileSample() noexcept = default; | |
| 112 | ProfileSample(const ProfileSample &) = delete; | ||
| 113 | ProfileSample &operator=(const ProfileSample &) = delete; | ||
| 114 | ProfileSample(ProfileSample &&) = delete; | ||
| 115 | ProfileSample &operator=(ProfileSample &&) = delete; | ||
| 116 | }; | ||
| 117 | |||
| 118 | /** | ||
| 119 | * @brief Converts a QPC tick interval to microseconds without overflow or undefined behaviour. | ||
| 120 | * @param start_ticks Interval start. | ||
| 121 | * @param end_ticks Interval end. Any ordering is accepted; a non-increasing interval converts to 0. | ||
| 122 | * @param frequency Ticks per second. A non-positive frequency converts to 0. | ||
| 123 | * @return Microseconds, saturated at UINT32_MAX. | ||
| 124 | * @details The difference is taken in unsigned arithmetic because `end_ticks - start_ticks` is undefined (not | ||
| 125 | * merely large) for extreme caller-supplied pairs such as a negative start with a positive end. The | ||
| 126 | * scaling is split into whole seconds plus remainder so the product never overflows: the direct | ||
| 127 | * `delta * 1'000'000` form wraps past a 10.7-day interval at a 10 MHz tick and reports a small duration | ||
| 128 | * for a huge one. | ||
| 129 | */ | ||
| 130 | [[nodiscard]] inline std::uint32_t | ||
| 131 | 386538 | ticks_to_microseconds(std::int64_t start_ticks, std::int64_t end_ticks, std::int64_t frequency) noexcept | |
| 132 | { | ||
| 133 | 386538 | constexpr std::uint64_t US_PER_SECOND = 1'000'000; | |
| 134 | 386538 | constexpr std::uint64_t MAX_US = UINT32_MAX; | |
| 135 | |||
| 136 |
2/4✓ Branch 2 → 3 taken 390341 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 391465 times.
|
386538 | if (frequency <= 0 || end_ticks <= start_ticks) |
| 137 | { | ||
| 138 | ✗ | return 0; | |
| 139 | } | ||
| 140 | |||
| 141 | 391465 | const std::uint64_t delta = static_cast<std::uint64_t>(end_ticks) - static_cast<std::uint64_t>(start_ticks); | |
| 142 | 391465 | const auto ticks_per_second = static_cast<std::uint64_t>(frequency); | |
| 143 | |||
| 144 | 391465 | const std::uint64_t whole_seconds = delta / ticks_per_second; | |
| 145 |
2/2✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 7 taken 391459 times.
|
391465 | if (whole_seconds > MAX_US / US_PER_SECOND) |
| 146 | { | ||
| 147 | 6 | return static_cast<std::uint32_t>(MAX_US); | |
| 148 | } | ||
| 149 | |||
| 150 | 391459 | const std::uint64_t remainder = delta % ticks_per_second; | |
| 151 | const std::uint64_t fraction_us = (remainder <= UINT64_MAX / US_PER_SECOND) | ||
| 152 |
1/2✓ Branch 7 → 8 taken 398178 times.
✗ Branch 7 → 9 not taken.
|
391459 | ? (remainder * US_PER_SECOND) / ticks_per_second |
| 153 | 391459 | : multiply_fraction(remainder, US_PER_SECOND, ticks_per_second); | |
| 154 | |||
| 155 | 406198 | const std::uint64_t micros = whole_seconds * US_PER_SECOND + fraction_us; | |
| 156 |
1/2✓ Branch 10 → 11 taken 406198 times.
✗ Branch 10 → 12 not taken.
|
406198 | return static_cast<std::uint32_t>(micros > MAX_US ? MAX_US : micros); |
| 157 | } | ||
| 158 | |||
| 159 | /** | ||
| 160 | * @brief Fixed-capacity sample ring with lock-free claim/publish and drop-on-collision. | ||
| 161 | * | ||
| 162 | * @details A writer claims a slot with one CAS and publishes into it. A claim whose slot another writer still | ||
| 163 | * owns, or whose slot a later writer already committed to, is refused and counted instead of clobbering. | ||
| 164 | * That refusal is what makes the exporter's before/after ticket comparison a proof rather than a | ||
| 165 | * heuristic: no sequence of collisions can leave a torn payload behind an unchanged word. | ||
| 166 | * | ||
| 167 | * Construction never throws. A ring that cannot allocate its slots, or that is asked for a capacity that | ||
| 168 | * is zero or not a power of two, reports capacity 0 and drops every claim. | ||
| 169 | * | ||
| 170 | * **Thread safety:** `claim` / `publish` are lock-free and callable from any thread. `visit_committed` is safe | ||
| 171 | * concurrently with them. `reset` requires that no claim is in flight. | ||
| 172 | */ | ||
| 173 | class ProfileRing | ||
| 174 | { | ||
| 175 | public: | ||
| 176 | /** | ||
| 177 | * @brief A slot reservation. | ||
| 178 | * @details `owned` is false for a refused claim; passing such a claim to @ref publish is a safe no-op. | ||
| 179 | */ | ||
| 180 | struct Claim | ||
| 181 | { | ||
| 182 | /// Ring slot index owned by the claim. | ||
| 183 | std::size_t index{0}; | ||
| 184 | /// Global write position represented by the claim. | ||
| 185 | std::uint64_t ticket{0}; | ||
| 186 | /// True only when the slot was reserved successfully. | ||
| 187 | bool owned{false}; | ||
| 188 | }; | ||
| 189 | |||
| 190 | /** | ||
| 191 | * @brief Allocates @p capacity slots. | ||
| 192 | * @param capacity Slot count; must be a power of two. Zero, a non-power-of-two, or an allocation failure yields | ||
| 193 | * an inert ring. | ||
| 194 | */ | ||
| 195 | 49 | explicit ProfileRing(std::size_t capacity) noexcept | |
| 196 | 49 | { | |
| 197 |
4/4✓ Branch 5 → 6 taken 48 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 47 times.
|
49 | if (capacity == 0 || (capacity & (capacity - 1)) != 0) |
| 198 | { | ||
| 199 | 2 | return; | |
| 200 | } | ||
| 201 |
8/10✓ Branch 8 → 9 taken 47 times.
✗ Branch 8 → 10 not taken.
✓ Branch 12 → 13 taken 45 times.
✓ Branch 12 → 18 taken 2 times.
✓ Branch 16 → 14 taken 2555922 times.
✓ Branch 16 → 17 taken 45 times.
✓ Branch 20 → 21 taken 45 times.
✓ Branch 20 → 23 taken 2 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 45 times.
|
2555969 | m_slots.reset(::new (std::nothrow) ProfileSample[capacity]); |
| 202 |
2/2✓ Branch 24 → 25 taken 45 times.
✓ Branch 24 → 26 taken 2 times.
|
47 | if (m_slots) |
| 203 | { | ||
| 204 | 45 | m_capacity = capacity; | |
| 205 | 45 | m_mask = capacity - 1; | |
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | ProfileRing(const ProfileRing &) = delete; | ||
| 210 | ProfileRing &operator=(const ProfileRing &) = delete; | ||
| 211 | ProfileRing(ProfileRing &&) = delete; | ||
| 212 | ProfileRing &operator=(ProfileRing &&) = delete; | ||
| 213 | 8 | ~ProfileRing() noexcept = default; | |
| 214 | |||
| 215 | /// Takes the next ring position without inspecting its slot. Complete it exactly once through @ref claim_at. | ||
| 216 | 387408 | [[nodiscard]] std::uint64_t reserve_position() noexcept | |
| 217 | { | ||
| 218 | 774816 | return m_write_pos.fetch_add(1, std::memory_order_relaxed); | |
| 219 | } | ||
| 220 | |||
| 221 | /// Reserves the next slot, or returns a refused claim and counts a drop. | ||
| 222 | 392329 | [[nodiscard]] Claim claim() noexcept { return claim_at(reserve_position()); } | |
| 223 | |||
| 224 | /** | ||
| 225 | * @brief Completes a reservation for an already-issued ring @p position. | ||
| 226 | * @details The second half of @ref claim, split out because a writer descheduled between taking its position | ||
| 227 | * and inspecting its slot is exactly the collision the drop rule exists for; driving this directly is | ||
| 228 | * the only way to reproduce it deterministically. Callers other than @ref claim must pass a position | ||
| 229 | * returned by @ref reserve_position exactly once, since this function does not advance the ring. | ||
| 230 | */ | ||
| 231 | 402066 | [[nodiscard]] Claim claim_at(std::uint64_t position) noexcept | |
| 232 | { | ||
| 233 |
3/4✓ Branch 2 → 3 taken 395265 times.
✓ Branch 2 → 4 taken 6801 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 410697 times.
|
402066 | if (m_capacity == 0 || position > MAX_POSITION) |
| 234 | { | ||
| 235 | 4 | m_dropped.fetch_add(1, std::memory_order_relaxed); | |
| 236 | 4 | return Claim{}; | |
| 237 | } | ||
| 238 | |||
| 239 | 410697 | const auto index = static_cast<std::size_t>(position & m_mask); | |
| 240 | 410697 | std::atomic<std::uint64_t> &state = m_slots[index].state; | |
| 241 | |||
| 242 | // Refuse rather than overwrite in both collision directions: an odd word means an earlier writer still | ||
| 243 | // owns the slot, and a committed ticket above ours means a later writer already published here while this | ||
| 244 | // one was descheduled for a full ring cycle. | ||
| 245 | 382762 | std::uint64_t observed = state.load(std::memory_order_acquire); | |
| 246 | 382475 | const std::uint64_t encoded_ticket = position + TICKET_OFFSET; | |
| 247 |
5/6✓ Branch 15 → 16 taken 387863 times.
✗ Branch 15 → 18 not taken.
✓ Branch 16 → 17 taken 190683 times.
✓ Branch 16 → 21 taken 197180 times.
✓ Branch 17 → 18 taken 5392 times.
✓ Branch 17 → 21 taken 185291 times.
|
382475 | if ((observed & BUSY_BIT) != 0 || (observed != EMPTY_STATE && (observed >> 1) >= encoded_ticket)) |
| 248 | { | ||
| 249 | 4 | m_dropped.fetch_add(1, std::memory_order_relaxed); | |
| 250 | 4 | return Claim{}; | |
| 251 | } | ||
| 252 | |||
| 253 | 382471 | const std::uint64_t owned_word = state_word(position, true); | |
| 254 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 31 taken 409672 times.
|
796254 | if (!state.compare_exchange_strong( |
| 255 | observed, | ||
| 256 | owned_word, | ||
| 257 | std::memory_order_acq_rel, | ||
| 258 | std::memory_order_relaxed | ||
| 259 | )) | ||
| 260 | { | ||
| 261 | ✗ | m_dropped.fetch_add(1, std::memory_order_relaxed); | |
| 262 | ✗ | return Claim{}; | |
| 263 | } | ||
| 264 | return Claim{ | ||
| 265 | .index = index, | ||
| 266 | .ticket = position, | ||
| 267 | .owned = true, | ||
| 268 | 409672 | }; | |
| 269 | } | ||
| 270 | |||
| 271 | /// Writes the payload into a claimed slot and commits it. A refused claim publishes nothing. | ||
| 272 | 389644 | void publish( | |
| 273 | const Claim &claim, | ||
| 274 | const char *name, | ||
| 275 | std::uint32_t name_length, | ||
| 276 | std::int64_t start_ticks, | ||
| 277 | std::uint32_t duration_us, | ||
| 278 | std::uint32_t thread_id | ||
| 279 | ) noexcept | ||
| 280 | { | ||
| 281 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 389642 times.
|
389644 | if (!claim.owned) |
| 282 | { | ||
| 283 | 2 | return; | |
| 284 | } | ||
| 285 | 389642 | ProfileSample &slot = m_slots[claim.index]; | |
| 286 | |||
| 287 | // The payload is published through std::atomic_ref because the exporter reads the same fields | ||
| 288 | // concurrently. Relaxed is sufficient: the release store on the ticket word below is what orders these | ||
| 289 | // writes for a reader that accepts the slot. | ||
| 290 | 387274 | std::atomic_ref<const char *>(slot.name).store(name, std::memory_order_relaxed); | |
| 291 | 422897 | std::atomic_ref<std::uint32_t>(slot.name_length).store(name_length, std::memory_order_relaxed); | |
| 292 | 422357 | std::atomic_ref<std::int64_t>(slot.start_ticks).store(start_ticks, std::memory_order_relaxed); | |
| 293 | 407325 | std::atomic_ref<std::uint32_t>(slot.duration_us).store(duration_us, std::memory_order_relaxed); | |
| 294 | 378590 | std::atomic_ref<std::uint32_t>(slot.thread_id).store(thread_id, std::memory_order_relaxed); | |
| 295 | |||
| 296 | 415679 | slot.state.store(state_word(claim.ticket, false), std::memory_order_release); | |
| 297 | } | ||
| 298 | |||
| 299 | /** | ||
| 300 | * @brief Invokes @p visitor for each committed sample in ring traversal order. | ||
| 301 | * @param visitor Called as `visitor(name, name_length, start_ticks, duration_us, thread_id)`. | ||
| 302 | */ | ||
| 303 | 33 | template <typename Visitor> void visit_committed(Visitor &&visitor) const | |
| 304 | { | ||
| 305 | 33 | const std::uint64_t total = m_write_pos.load(std::memory_order_relaxed); | |
| 306 |
3/4void DetourModKit::detail::ProfileRing::visit_committed<(anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>((anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 5 times.
void DetourModKit::detail::ProfileRing::visit_committed<DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>(DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 24 times.
|
33 | const std::uint64_t resident = total < m_capacity ? total : m_capacity; |
| 307 |
3/4void DetourModKit::detail::ProfileRing::visit_committed<(anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>((anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 5 times.
void DetourModKit::detail::ProfileRing::visit_committed<DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>(DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 24 times.
|
33 | const std::size_t start = total > m_capacity ? static_cast<std::size_t>(total & m_mask) : 0; |
| 308 | |||
| 309 |
4/4void DetourModKit::detail::ProfileRing::visit_committed<(anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>((anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 51 → 16 taken 16 times.
✓ Branch 51 → 52 taken 9 times.
void DetourModKit::detail::ProfileRing::visit_committed<DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>(DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 51 → 16 taken 59387 times.
✓ Branch 51 → 52 taken 24 times.
|
59436 | for (std::uint64_t i = 0; i < resident; ++i) |
| 310 | { | ||
| 311 | 59403 | ProfileSample &slot = m_slots[(start + static_cast<std::size_t>(i)) & m_mask]; | |
| 312 | |||
| 313 | 59403 | const std::uint64_t before = slot.state.load(std::memory_order_acquire); | |
| 314 |
6/8void DetourModKit::detail::ProfileRing::visit_committed<(anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>((anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 24 → 25 taken 16 times.
✗ Branch 24 → 26 not taken.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 15 times.
void DetourModKit::detail::ProfileRing::visit_committed<DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>(DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 24 → 25 taken 59387 times.
✗ Branch 24 → 26 not taken.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 59386 times.
|
59403 | if (before == EMPTY_STATE || (before & BUSY_BIT) != 0) |
| 315 | { | ||
| 316 | 2 | continue; | |
| 317 | } | ||
| 318 | 59401 | const char *const name = std::atomic_ref<const char *>(slot.name).load(std::memory_order_relaxed); | |
| 319 |
2/4void DetourModKit::detail::ProfileRing::visit_committed<(anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>((anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 15 times.
void DetourModKit::detail::ProfileRing::visit_committed<DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>(DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 59386 times.
|
59401 | if (name == nullptr) |
| 320 | { | ||
| 321 | ✗ | continue; | |
| 322 | } | ||
| 323 | const auto name_length = | ||
| 324 | 59401 | std::atomic_ref<std::uint32_t>(slot.name_length).load(std::memory_order_relaxed); | |
| 325 | const auto start_ticks = | ||
| 326 | 59401 | std::atomic_ref<std::int64_t>(slot.start_ticks).load(std::memory_order_relaxed); | |
| 327 | const auto duration_us = | ||
| 328 | 59401 | std::atomic_ref<std::uint32_t>(slot.duration_us).load(std::memory_order_relaxed); | |
| 329 | 59401 | const auto thread_id = std::atomic_ref<std::uint32_t>(slot.thread_id).load(std::memory_order_relaxed); | |
| 330 | |||
| 331 | std::atomic_thread_fence(std::memory_order_acquire); | ||
| 332 |
3/4void DetourModKit::detail::ProfileRing::visit_committed<(anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>((anonymous namespace)::collect(DetourModKit::detail::ProfileRing const&)::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 15 times.
void DetourModKit::detail::ProfileRing::visit_committed<DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}>(DetourModKit::Profiler::export_chrome_json[abi:cxx11]() const::{lambda(char const*, unsigned int, long long, unsigned int, unsigned int)#1}&&) const:
✓ Branch 47 → 48 taken 2 times.
✓ Branch 47 → 49 taken 59384 times.
|
118802 | if (slot.state.load(std::memory_order_relaxed) != before) |
| 333 | { | ||
| 334 | 2 | continue; | |
| 335 | } | ||
| 336 | 59399 | visitor(name, name_length, start_ticks, duration_us, thread_id); | |
| 337 | } | ||
| 338 | 33 | } | |
| 339 | |||
| 340 | /// Discards every sample and restarts ticketing. Requires that no claim is in flight. | ||
| 341 | 73 | void reset() noexcept | |
| 342 | { | ||
| 343 | 73 | m_write_pos.store(0, std::memory_order_relaxed); | |
| 344 | 73 | m_dropped.store(0, std::memory_order_relaxed); | |
| 345 |
2/2✓ Branch 39 → 19 taken 4521986 times.
✓ Branch 39 → 40 taken 73 times.
|
4522059 | for (std::size_t i = 0; i < m_capacity; ++i) |
| 346 | { | ||
| 347 | 4521986 | ProfileSample &slot = m_slots[i]; | |
| 348 | 4521986 | std::atomic_ref<const char *>(slot.name).store(nullptr, std::memory_order_relaxed); | |
| 349 | 4521986 | std::atomic_ref<std::uint32_t>(slot.name_length).store(0, std::memory_order_relaxed); | |
| 350 | 4521986 | std::atomic_ref<std::int64_t>(slot.start_ticks).store(0, std::memory_order_relaxed); | |
| 351 | 4521986 | std::atomic_ref<std::uint32_t>(slot.duration_us).store(0, std::memory_order_relaxed); | |
| 352 | 4521986 | std::atomic_ref<std::uint32_t>(slot.thread_id).store(0, std::memory_order_relaxed); | |
| 353 | 4521986 | slot.state.store(0, std::memory_order_relaxed); | |
| 354 | } | ||
| 355 | 73 | } | |
| 356 | |||
| 357 | /// Slot count, or 0 for an inert ring. | ||
| 358 | 14 | [[nodiscard]] std::size_t capacity() const noexcept { return m_capacity; } | |
| 359 | |||
| 360 | /// Claims attempted since construction or the last @ref reset, including refused ones. | ||
| 361 | 82 | [[nodiscard]] std::uint64_t claims() const noexcept { return m_write_pos.load(std::memory_order_relaxed); } | |
| 362 | |||
| 363 | /// Claims refused because the slot was owned, already newer, or the ring is inert. | ||
| 364 | 24 | [[nodiscard]] std::uint64_t dropped() const noexcept { return m_dropped.load(std::memory_order_relaxed); } | |
| 365 | |||
| 366 | /// Committed samples still resident. Exact when no claim is in flight. | ||
| 367 | 47 | [[nodiscard]] std::uint64_t resident() const noexcept | |
| 368 | { | ||
| 369 | 47 | const std::uint64_t dropped_now = m_dropped.load(std::memory_order_relaxed); | |
| 370 | 47 | const std::uint64_t claims_now = m_write_pos.load(std::memory_order_relaxed); | |
| 371 |
2/2✓ Branch 16 → 17 taken 33 times.
✓ Branch 16 → 18 taken 14 times.
|
47 | const std::uint64_t committed = claims_now > dropped_now ? claims_now - dropped_now : 0; |
| 372 |
2/2✓ Branch 19 → 20 taken 11 times.
✓ Branch 19 → 21 taken 36 times.
|
47 | return committed < m_capacity ? committed : m_capacity; |
| 373 | } | ||
| 374 | |||
| 375 | private: | ||
| 376 | static constexpr std::uint64_t BUSY_BIT{1}; | ||
| 377 | static constexpr std::uint64_t TICKET_OFFSET{1}; | ||
| 378 | static constexpr std::uint64_t EMPTY_STATE{0}; | ||
| 379 | static constexpr std::uint64_t MAX_POSITION{(UINT64_MAX >> 1) - 1}; | ||
| 380 | |||
| 381 | 762390 | [[nodiscard]] static constexpr std::uint64_t state_word(std::uint64_t position, bool busy) noexcept | |
| 382 | { | ||
| 383 | 762390 | return ((position + TICKET_OFFSET) << 1) | static_cast<std::uint64_t>(busy); | |
| 384 | } | ||
| 385 | |||
| 386 | static_assert((TICKET_OFFSET << 1) != EMPTY_STATE, "the first committed ticket must differ from an empty slot"); | ||
| 387 | static_assert( | ||
| 388 | std::atomic<std::uint64_t>::is_always_lock_free, | ||
| 389 | "the profile ring requires lock-free 64-bit atomics" | ||
| 390 | ); | ||
| 391 | static_assert( | ||
| 392 | std::atomic_ref<const char *>::is_always_lock_free, | ||
| 393 | "the profile ring requires lock-free pointer publication" | ||
| 394 | ); | ||
| 395 | static_assert( | ||
| 396 | std::atomic_ref<std::int64_t>::is_always_lock_free, | ||
| 397 | "the profile ring requires lock-free 64-bit payload publication" | ||
| 398 | ); | ||
| 399 | static_assert( | ||
| 400 | std::atomic_ref<std::uint32_t>::is_always_lock_free, | ||
| 401 | "the profile ring requires lock-free 32-bit payload publication" | ||
| 402 | ); | ||
| 403 | |||
| 404 | alignas(64) std::atomic<std::uint64_t> m_write_pos{0}; | ||
| 405 | std::atomic<std::uint64_t> m_dropped{0}; | ||
| 406 | std::unique_ptr<ProfileSample[]> m_slots; | ||
| 407 | std::size_t m_capacity{0}; | ||
| 408 | std::size_t m_mask{0}; | ||
| 409 | }; | ||
| 410 | } // namespace DetourModKit::detail | ||
| 411 | |||
| 412 | #endif // DETOURMODKIT_DETAIL_PROFILE_RING_HPP | ||
| 413 |