GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 95.6% 131 / 0 / 137
Functions: 100.0% 17 / 0 / 17
Branches: 82.2% 74 / 0 / 90

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/4
void 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/4
void 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/4
void 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/8
void 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/4
void 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/4
void 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