GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.2% 369 / 0 / 396
Functions: 100.0% 37 / 0 / 37
Branches: 71.6% 154 / 0 / 215

src/async_logger.cpp
Line Branch Exec Source
1 #include "DetourModKit/async_logger.hpp"
2
3 #include <algorithm>
4 #include <cstring>
5 #include <iomanip>
6 #include <iostream>
7 #include <sstream>
8
9 namespace DetourModKit
10 {
11 1 StringPool::StringPool()
12 {
13
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 12 not taken.
1 std::lock_guard<std::mutex> lock(pool_mutex_);
14
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 10 not taken.
1 grow_pool_locked();
15 1 }
16
17 1 StringPool::~StringPool() noexcept
18 {
19 1 size_t leaked = 0;
20
21 {
22 // Acquire the mutex to synchronize with any in-flight deallocate() calls
23 1 std::lock_guard<std::mutex> lock(pool_mutex_);
24 1 leaked = heap_fallback_count_.load(std::memory_order_relaxed);
25 1 }
26
27
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 1 time.
1 if (leaked > 0)
28 {
29 std::cerr << "[StringPool] " << leaked
30 << " heap-fallback string(s) were not returned before destruction\n";
31 }
32
33 1 Block *current = head_.load(std::memory_order_relaxed);
34
2/2
✓ Branch 24 → 17 taken 2 times.
✓ Branch 24 → 25 taken 1 time.
3 while (current)
35 {
36 2 Block *next = current->next;
37
38 2 PoolSlot *slots = reinterpret_cast<PoolSlot *>(current->data);
39
2/2
✓ Branch 21 → 18 taken 32 times.
✓ Branch 21 → 22 taken 2 times.
34 for (size_t i = 0; i < POOL_SLOTS_PER_BLOCK; ++i)
40 {
41
1/2
✓ Branch 18 → 19 taken 32 times.
✗ Branch 18 → 20 not taken.
32 if (current->constructed_mask & (1u << i))
42 {
43 32 slots[i].~PoolSlot();
44 }
45 }
46
47 2 ::operator delete(current);
48 2 current = next;
49 }
50 1 head_.store(nullptr, std::memory_order_relaxed);
51 1 }
52
53 2 void StringPool::grow_pool_locked()
54 {
55 2 Block *existing = head_.load(std::memory_order_relaxed);
56 2 size_t count = 0;
57
2/2
✓ Branch 7 → 4 taken 1 time.
✓ Branch 7 → 8 taken 2 times.
3 for (Block *b = existing; b; b = b->next)
58 {
59
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (++count >= MEMORY_POOL_BLOCK_COUNT)
60 {
61 return;
62 }
63 }
64
65
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2 times.
2 Block *new_block = new (::operator new(sizeof(Block))) Block();
66
67 2 new_block->next = existing;
68 2 new_block->free_list = nullptr;
69 2 new_block->slot_count = POOL_SLOTS_PER_BLOCK;
70
71 2 PoolSlot *slots = reinterpret_cast<PoolSlot *>(new_block->data);
72 static_assert(POOL_SLOTS_PER_BLOCK <= 32, "constructed_mask is uint32_t; increase its width if POOL_SLOTS_PER_BLOCK > 32");
73 2 uint32_t constructed = 0;
74
2/2
✓ Branch 22 → 14 taken 32 times.
✓ Branch 22 → 23 taken 2 times.
34 for (size_t i = 0; i < POOL_SLOTS_PER_BLOCK; ++i)
75 {
76
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 32 times.
32 new (&slots[i]) PoolSlot();
77 32 constructed |= (1u << i);
78
2/2
✓ Branch 18 → 19 taken 30 times.
✓ Branch 18 → 20 taken 2 times.
32 slots[i].next_free = (i + 1 < POOL_SLOTS_PER_BLOCK) ? &slots[i + 1] : nullptr;
79 }
80 2 new_block->constructed_mask = constructed;
81 2 new_block->free_list = &slots[0];
82
83 2 head_.store(new_block, std::memory_order_release);
84 2 pool_size_.fetch_add(1, std::memory_order_relaxed);
85 }
86
87 23 StringPool &StringPool::instance() noexcept
88 {
89
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 22 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
23 static StringPool pool;
90 23 return pool;
91 }
92
93 443 StringPool::PoolSlot *StringPool::claim_free_slot() noexcept
94 {
95
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 443 times.
443 assert(!pool_mutex_.try_lock() && "claim_free_slot must be called with pool_mutex_ held");
96
2/2
✓ Branch 10 → 7 taken 443 times.
✓ Branch 10 → 11 taken 1 time.
444 for (Block *b = head_.load(std::memory_order_relaxed); b; b = b->next)
97 {
98
2/2
✓ Branch 7 → 8 taken 442 times.
✓ Branch 7 → 9 taken 1 time.
443 if (b->free_list)
99 {
100 442 PoolSlot *slot = b->free_list;
101 442 b->free_list = slot->next_free;
102 442 --b->slot_count;
103 442 return slot;
104 }
105 }
106 1 return nullptr;
107 }
108
109 445 std::string *StringPool::allocate(size_t size)
110 {
111
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 16 taken 442 times.
445 if (size > MEMORY_POOL_BLOCK_SIZE - sizeof(PoolSlot) - 16)
112 {
113
3/6
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 7 not taken.
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 11 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3 times.
3 auto *ptr = new (std::nothrow) std::string();
114
1/2
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 15 not taken.
3 if (ptr)
115 {
116 3 heap_fallback_count_.fetch_add(1, std::memory_order_relaxed);
117 }
118 3 return ptr;
119 }
120
121
1/2
✓ Branch 16 → 17 taken 442 times.
✗ Branch 16 → 42 not taken.
442 std::lock_guard<std::mutex> lock(pool_mutex_);
122
123 442 PoolSlot *slot = claim_free_slot();
124
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 21 taken 441 times.
442 if (!slot)
125 {
126
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 40 not taken.
1 grow_pool_locked();
127 1 slot = claim_free_slot();
128 }
129
130
1/2
✓ Branch 21 → 22 taken 442 times.
✗ Branch 21 → 24 not taken.
442 if (slot)
131 {
132 442 slot->str.clear();
133 442 return &slot->str;
134 }
135
136 auto *ptr = new (std::nothrow) std::string();
137 if (ptr)
138 {
139 heap_fallback_count_.fetch_add(1, std::memory_order_relaxed);
140 }
141 return ptr;
142 442 }
143
144 441 void StringPool::deallocate(std::string *ptr) noexcept
145 {
146
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 441 times.
441 if (!ptr)
147 442 return;
148
149 441 std::lock_guard<std::mutex> lock(pool_mutex_);
150
151
2/2
✓ Branch 12 → 6 taken 462 times.
✓ Branch 12 → 13 taken 3 times.
465 for (Block *b = head_.load(std::memory_order_relaxed); b; b = b->next)
152 {
153 462 const auto *block_begin = reinterpret_cast<const char *>(b->data);
154 462 const auto *block_end = block_begin + POOL_SLOTS_PER_BLOCK * sizeof(PoolSlot);
155 462 const auto *raw_ptr = reinterpret_cast<const char *>(ptr);
156
157
4/4
✓ Branch 6 → 7 taken 445 times.
✓ Branch 6 → 11 taken 17 times.
✓ Branch 7 → 8 taken 442 times.
✓ Branch 7 → 11 taken 3 times.
462 if (raw_ptr >= block_begin && raw_ptr < block_end)
158 {
159 442 auto offset = static_cast<size_t>(raw_ptr - block_begin);
160 442 PoolSlot *slot = reinterpret_cast<PoolSlot *>(b->data) + (offset / sizeof(PoolSlot));
161 442 slot->str.clear();
162 442 return_slot_locked(slot, b);
163 442 return;
164 }
165 }
166
167 // Not a pool allocation — heap delete under lock is safe and brief.
168
1/2
✓ Branch 13 → 14 taken 3 times.
✗ Branch 13 → 16 not taken.
3 delete ptr;
169
1/2
✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 27 not taken.
6 if (heap_fallback_count_.load(std::memory_order_relaxed) > 0)
170 {
171 3 heap_fallback_count_.fetch_sub(1, std::memory_order_relaxed);
172 }
173
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 32 taken 442 times.
445 }
174
175 442 void StringPool::return_slot_locked(PoolSlot *slot, Block *block) noexcept
176 {
177 442 slot->next_free = block->free_list;
178 442 block->free_list = slot;
179 442 ++block->slot_count;
180 442 }
181
182 5962 LogMessage::LogMessage(LogLevel lvl, std::string_view msg)
183 5962 : level(lvl),
184 5962 timestamp(std::chrono::system_clock::now()),
185 6014 thread_id(std::this_thread::get_id())
186 {
187 5981 const size_t msg_size = std::min(msg.size(), MAX_VALID_LENGTH);
188
189
2/2
✓ Branch 6 → 7 taken 5929 times.
✓ Branch 6 → 11 taken 9 times.
5938 if (msg_size <= MAX_INLINE_SIZE)
190 {
191 5929 std::memcpy(buffer.data(), msg.data(), msg_size);
192 5937 length = msg_size;
193 }
194 else
195 {
196 9 overflow = StringPool::instance().allocate(msg_size);
197
1/2
✓ Branch 13 → 14 taken 9 times.
✗ Branch 13 → 18 not taken.
9 if (overflow)
198 {
199 try
200 {
201
1/2
✓ Branch 15 → 16 taken 9 times.
✗ Branch 15 → 20 not taken.
9 overflow->assign(msg.data(), msg_size);
202 9 length = overflow->size();
203 }
204 catch (...)
205 {
206 StringPool::instance().deallocate(overflow);
207 overflow = nullptr;
208 length = 0;
209 }
210 }
211 else
212 {
213 // Allocation failed (OOM) — message is silently dropped
214 length = 0;
215 }
216 }
217 5946 }
218
219 346563 LogMessage::~LogMessage() noexcept
220 {
221 346563 reset();
222 346554 }
223
224 2364 LogMessage::LogMessage(LogMessage &&other) noexcept
225 2364 : level(other.level),
226 2364 timestamp(other.timestamp),
227 2364 thread_id(other.thread_id),
228 2364 length(other.length),
229 2364 overflow(other.overflow)
230 {
231
4/4
✓ Branch 2 → 3 taken 2362 times.
✓ Branch 2 → 9 taken 2 times.
✓ Branch 3 → 4 taken 2360 times.
✓ Branch 3 → 9 taken 2 times.
2364 if (length > 0 && !overflow)
232 {
233 7080 std::memcpy(buffer.data(), other.buffer.data(), length);
234 }
235 2364 other.overflow = nullptr;
236 2364 other.length = 0;
237 2364 }
238
239 9111 LogMessage &LogMessage::operator=(LogMessage &&other) noexcept
240 {
241
1/2
✓ Branch 2 → 3 taken 9111 times.
✗ Branch 2 → 12 not taken.
9111 if (this != &other)
242 {
243 9111 reset();
244 9103 level = other.level;
245 9103 timestamp = other.timestamp;
246 9103 thread_id = other.thread_id;
247 9103 length = other.length;
248 9103 overflow = other.overflow;
249
3/4
✓ Branch 4 → 5 taken 9103 times.
✗ Branch 4 → 11 not taken.
✓ Branch 5 → 6 taken 9101 times.
✓ Branch 5 → 11 taken 2 times.
9103 if (length > 0 && !overflow)
250 {
251 27303 std::memcpy(buffer.data(), other.buffer.data(), length);
252 }
253 9103 other.overflow = nullptr;
254 9103 other.length = 0;
255 }
256 9103 return *this;
257 }
258
259 2571 std::string_view LogMessage::message() const noexcept
260 {
261
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 2564 times.
2571 if (overflow)
262 {
263 7 return *overflow;
264 }
265 2564 return std::string_view(buffer.data(), length);
266 }
267
268 19 bool LogMessage::is_valid() const noexcept
269 {
270
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 5 taken 14 times.
19 if (overflow)
271 {
272 5 return length == overflow->size();
273 }
274 14 return length <= MAX_INLINE_SIZE;
275 }
276
277 355567 void LogMessage::reset() noexcept
278 {
279
2/2
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 6 taken 355558 times.
355567 if (overflow)
280 {
281 9 StringPool::instance().deallocate(overflow);
282 9 overflow = nullptr;
283 }
284 355567 length = 0;
285 355567 }
286
287 80 size_t DynamicMPMCQueue::validated_capacity(size_t capacity)
288 {
289
4/4
✓ Branch 2 → 3 taken 74 times.
✓ Branch 2 → 4 taken 6 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 71 times.
80 if ((capacity & (capacity - 1)) != 0 || capacity < 2)
290 {
291
1/2
✓ Branch 5 → 6 taken 9 times.
✗ Branch 5 → 9 not taken.
9 throw std::invalid_argument("DynamicMPMCQueue capacity must be a power of 2 and at least 2");
292 }
293 71 return capacity;
294 }
295
296 80 DynamicMPMCQueue::DynamicMPMCQueue(size_t capacity)
297 80 : capacity_(validated_capacity(capacity)), mask_(capacity_ - 1),
298 71 buffer_(std::make_unique<Slot[]>(capacity_))
299 {
300
2/2
✓ Branch 17 → 7 taken 337762 times.
✓ Branch 17 → 18 taken 71 times.
337833 for (size_t i = 0; i < capacity_; ++i)
301 {
302 337762 buffer_[i].sequence.store(i, std::memory_order_relaxed);
303 }
304 71 }
305
306 7494 bool DynamicMPMCQueue::try_push(LogMessage &item)
307 {
308 14987 size_t pos = enqueue_pos_.load(std::memory_order_relaxed);
309
310 for (;;)
311 {
312 8228 Slot &slot = buffer_[pos & mask_];
313 8240 size_t seq = slot.sequence.load(std::memory_order_acquire);
314 8256 intptr_t diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos);
315
316
2/2
✓ Branch 18 → 19 taken 5238 times.
✓ Branch 18 → 40 taken 3018 times.
8256 if (diff == 0)
317 {
318
2/2
✓ Branch 27 → 28 taken 4572 times.
✓ Branch 27 → 50 taken 691 times.
10501 if (enqueue_pos_.compare_exchange_weak(pos, pos + 1,
319 std::memory_order_relaxed))
320 {
321 4572 slot.data = std::move(item);
322 4567 slot.sequence.store(pos + 1, std::memory_order_release);
323 4570 return true;
324 }
325 }
326
2/2
✓ Branch 40 → 41 taken 2998 times.
✓ Branch 40 → 42 taken 20 times.
3018 else if (diff < 0)
327 {
328 2998 return false;
329 }
330 else
331 {
332 64 pos = enqueue_pos_.load(std::memory_order_relaxed);
333 }
334 735 }
335 }
336
337 4889 bool DynamicMPMCQueue::try_pop(LogMessage &item)
338 {
339 9774 size_t pos = dequeue_pos_.load(std::memory_order_relaxed);
340
341 for (;;)
342 {
343 5931 Slot &slot = buffer_[pos & mask_];
344 5765 size_t seq = slot.sequence.load(std::memory_order_acquire);
345 5721 intptr_t diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos + 1);
346
347
2/2
✓ Branch 18 → 19 taken 5316 times.
✓ Branch 18 → 40 taken 405 times.
5721 if (diff == 0)
348 {
349
2/2
✓ Branch 27 → 28 taken 4566 times.
✓ Branch 27 → 50 taken 870 times.
10752 if (dequeue_pos_.compare_exchange_weak(pos, pos + 1,
350 std::memory_order_relaxed))
351 {
352 9132 item = std::move(slot.data);
353 4566 slot.sequence.store(pos + capacity_, std::memory_order_release);
354 4566 return true;
355 }
356 }
357
2/2
✓ Branch 40 → 41 taken 314 times.
✓ Branch 40 → 42 taken 91 times.
405 else if (diff < 0)
358 {
359 314 return false;
360 }
361 else
362 {
363 267 pos = dequeue_pos_.load(std::memory_order_relaxed);
364 }
365 1046 }
366 }
367
368 181 size_t DynamicMPMCQueue::try_pop_batch(std::vector<LogMessage> &items, size_t max_count)
369 {
370
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 180 times.
181 if (max_count == 0)
371 {
372 1 return 0;
373 }
374
375
1/2
✓ Branch 5 → 6 taken 180 times.
✗ Branch 5 → 23 not taken.
180 items.reserve(items.size() + max_count);
376
377 180 size_t count = 0;
378 180 LogMessage msg;
379
380
7/8
✓ Branch 12 → 13 taken 2454 times.
✓ Branch 12 → 16 taken 82 times.
✓ Branch 13 → 14 taken 2454 times.
✗ Branch 13 → 21 not taken.
✓ Branch 14 → 15 taken 2356 times.
✓ Branch 14 → 16 taken 98 times.
✓ Branch 17 → 8 taken 2356 times.
✓ Branch 17 → 18 taken 180 times.
2536 while (count < max_count && try_pop(msg))
381 {
382
1/2
✓ Branch 10 → 11 taken 2356 times.
✗ Branch 10 → 21 not taken.
2356 items.push_back(std::move(msg));
383 2356 ++count;
384 }
385
386 180 return count;
387 180 }
388
389 190 size_t DynamicMPMCQueue::size() const noexcept
390 {
391 190 size_t enq = enqueue_pos_.load(std::memory_order_relaxed);
392 190 size_t deq = dequeue_pos_.load(std::memory_order_relaxed);
393
1/2
✓ Branch 16 → 17 taken 190 times.
✗ Branch 16 → 18 not taken.
190 return (enq >= deq) ? (enq - deq) : 0;
394 }
395
396 182 bool DynamicMPMCQueue::empty() const noexcept
397 {
398 182 return size() == 0;
399 }
400
401 61 AsyncLogger::AsyncLogger(const AsyncLoggerConfig &config,
402 std::shared_ptr<WinFileStream> file_stream,
403 61 std::shared_ptr<std::mutex> log_mutex)
404 61 : queue_(config.queue_capacity),
405 58 config_(config),
406 116 file_stream_(std::move(file_stream)),
407 116 log_mutex_(std::move(log_mutex))
408 {
409
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 21 taken 58 times.
58 if (!config_.validate())
410 {
411 throw std::invalid_argument("Invalid AsyncLoggerConfig");
412 }
413
414
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 26 taken 57 times.
58 if (!file_stream_)
415 {
416
1/2
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 38 not taken.
1 throw std::invalid_argument("file_stream cannot be null");
417 }
418
419
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 31 taken 56 times.
57 if (!log_mutex_)
420 {
421
1/2
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 40 not taken.
1 throw std::invalid_argument("log_mutex cannot be null");
422 }
423
424 56 running_.store(true, std::memory_order_release);
425
1/2
✓ Branch 32 → 33 taken 56 times.
✗ Branch 32 → 42 not taken.
56 writer_thread_ = std::jthread(&AsyncLogger::writer_thread_func, this);
426 68 }
427
428 56 AsyncLogger::~AsyncLogger() noexcept
429 {
430 56 shutdown();
431 56 }
432
433 3956 bool AsyncLogger::enqueue(LogLevel level, std::string_view message) noexcept
434 {
435
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 53 taken 3968 times.
3956 if (shutdown_requested_.load(std::memory_order_acquire))
436 {
437 4 std::lock_guard<std::mutex> lock(*log_mutex_);
438
3/6
✓ Branch 8 → 9 taken 4 times.
✗ Branch 8 → 13 not taken.
✓ Branch 11 → 12 taken 4 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 51 not taken.
4 if (file_stream_->is_open() && file_stream_->good())
439 {
440 4 const auto now = std::chrono::system_clock::now();
441 4 const auto time_t = std::chrono::system_clock::to_time_t(now);
442 4 std::tm tm_buf{};
443
444 #if defined(_WIN32) || defined(_MSC_VER)
445 4 localtime_s(&tm_buf, &time_t);
446 #else
447 localtime_r(&time_t, &tm_buf);
448 #endif
449
450 4 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
451 8 now.time_since_epoch()) %
452 8 1000;
453 4 *file_stream_ << "[" << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S")
454 4 << "." << std::setfill('0') << std::setw(3) << ms.count()
455 << std::setfill(' ') << "] "
456 4 << "[" << std::setw(7) << std::left << log_level_to_string(level) << "] :: "
457 4 << message << '\n';
458 4 file_stream_->flush();
459 }
460 4 return true;
461 4 }
462
463 3968 LogMessage msg(level, message);
464
465 // Increment before push so flush cannot observe zero while a message
466 // is already in the queue but not yet counted.
467 3932 pending_messages_.fetch_add(1, std::memory_order_acq_rel);
468
2/2
✓ Branch 57 → 58 taken 2321 times.
✓ Branch 57 → 60 taken 1660 times.
3932 if (queue_.try_push(msg))
469 {
470 2321 flush_cv_.notify_one();
471 2336 return true;
472 }
473 // Push failed — undo the pre-increment before entering overflow handling
474 1660 pending_messages_.fetch_sub(1, std::memory_order_acq_rel);
475 1660 return handle_overflow(std::move(msg));
476 4003 }
477
478 14 bool AsyncLogger::flush_with_timeout(std::chrono::milliseconds timeout) noexcept
479 {
480
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 11 times.
14 if (!running_.load(std::memory_order_acquire))
481 {
482 3 return true;
483 }
484
485 11 std::unique_lock<std::mutex> lock(flush_mutex_);
486
487 11 const bool flushed = flush_cv_.wait_for(lock, timeout, [this]() noexcept
488 190 { return pending_messages_.load(std::memory_order_acquire) == 0; });
489
490 11 return flushed;
491 11 }
492
493 9 void AsyncLogger::flush() noexcept
494 {
495 9 static_cast<void>(flush_with_timeout(DEFAULT_FLUSH_TIMEOUT));
496 9 }
497
498 111 void AsyncLogger::shutdown() noexcept
499 {
500 111 bool expected = false;
501
2/2
✓ Branch 3 → 4 taken 55 times.
✓ Branch 3 → 5 taken 56 times.
111 if (!shutdown_requested_.compare_exchange_strong(expected, true,
502 std::memory_order_acq_rel))
503 {
504 55 return;
505 }
506
507 56 running_.store(false, std::memory_order_release);
508 56 flush_cv_.notify_all();
509
510
1/2
✓ Branch 8 → 9 taken 56 times.
✗ Branch 8 → 10 not taken.
56 if (writer_thread_.joinable())
511 {
512 56 writer_thread_.join();
513 }
514
515 {
516 56 std::lock_guard<std::mutex> lock(flush_mutex_);
517 56 pending_messages_.store(0, std::memory_order_release);
518 56 flush_cv_.notify_all();
519 56 }
520 }
521
522 7 bool AsyncLogger::is_running() const noexcept
523 {
524 7 return running_.load(std::memory_order_acquire);
525 }
526
527 2 size_t AsyncLogger::queue_size() const noexcept
528 {
529 2 return queue_.size();
530 }
531
532 7 size_t AsyncLogger::dropped_count() const noexcept
533 {
534 14 return dropped_messages_.load(std::memory_order_relaxed);
535 }
536
537 1 void AsyncLogger::reset_dropped_count() noexcept
538 {
539 1 dropped_messages_.store(0, std::memory_order_release);
540 1 }
541
542 56 void AsyncLogger::writer_thread_func() noexcept
543 {
544 56 std::vector<LogMessage> batch;
545 56 batch.reserve(config_.batch_size);
546
547 56 const auto start_time = std::chrono::steady_clock::now();
548 56 auto last_flush = start_time;
549
550
6/6
✓ Branch 38 → 39 taken 70 times.
✓ Branch 38 → 41 taken 162 times.
✓ Branch 40 → 41 taken 14 times.
✓ Branch 40 → 42 taken 56 times.
✓ Branch 43 → 5 taken 176 times.
✓ Branch 43 → 44 taken 56 times.
232 while (running_.load(std::memory_order_acquire) || !queue_.empty())
551 {
552 176 batch.clear();
553 176 queue_.try_pop_batch(batch, config_.batch_size);
554
555
2/2
✓ Branch 8 → 9 taken 125 times.
✓ Branch 8 → 18 taken 51 times.
176 if (!batch.empty())
556 {
557 125 write_batch(batch);
558 125 const size_t batch_size = batch.size();
559 {
560 125 std::lock_guard<std::mutex> flock(flush_mutex_);
561 125 pending_messages_.fetch_sub(batch_size, std::memory_order_acq_rel);
562 125 }
563 125 flush_cv_.notify_all();
564 125 last_flush = std::chrono::steady_clock::now();
565 }
566 else
567 {
568 51 auto now = std::chrono::steady_clock::now();
569
2/2
✓ Branch 22 → 23 taken 10 times.
✓ Branch 22 → 32 taken 41 times.
51 if (now - last_flush >= config_.flush_interval)
570 {
571 10 std::lock_guard<std::mutex> lock(*log_mutex_);
572
1/2
✓ Branch 27 → 28 taken 10 times.
✗ Branch 27 → 30 not taken.
10 if (file_stream_->is_open())
573 {
574 10 file_stream_->flush();
575 }
576 10 last_flush = now;
577 10 }
578
579 51 std::unique_lock<std::mutex> lock(flush_mutex_);
580 51 flush_cv_.wait_for(lock, config_.flush_interval, [this]()
581
4/4
✓ Branch 3 → 4 taken 97 times.
✓ Branch 3 → 6 taken 5 times.
✓ Branch 5 → 6 taken 36 times.
✓ Branch 5 → 7 taken 61 times.
102 { return !queue_.empty() || !running_.load(std::memory_order_acquire); });
582 51 }
583 }
584
585 {
586 56 std::lock_guard<std::mutex> lock(*log_mutex_);
587
1/2
✓ Branch 48 → 49 taken 56 times.
✗ Branch 48 → 51 not taken.
56 if (file_stream_->is_open())
588 {
589 56 file_stream_->flush();
590 }
591 56 }
592
593 {
594 56 std::lock_guard<std::mutex> lock(flush_mutex_);
595 56 pending_messages_.store(0, std::memory_order_release);
596 56 flush_cv_.notify_all();
597 56 }
598 56 }
599
600 125 void AsyncLogger::write_batch(std::span<LogMessage> messages) noexcept
601 {
602 125 std::lock_guard<std::mutex> lock(*log_mutex_);
603
604
3/6
✓ Branch 6 → 7 taken 125 times.
✗ Branch 6 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 125 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 125 times.
125 if (!file_stream_->is_open() || !file_stream_->good())
605 {
606 return;
607 }
608
609
2/2
✓ Branch 60 → 16 taken 2341 times.
✓ Branch 60 → 61 taken 125 times.
2591 for (const auto &msg : messages)
610 {
611 2341 const auto time_t = std::chrono::system_clock::to_time_t(msg.timestamp);
612 2341 std::tm tm_buf{};
613
614 #if defined(_WIN32) || defined(_MSC_VER)
615 2341 localtime_s(&tm_buf, &time_t);
616 #else
617 localtime_r(&time_t, &tm_buf);
618 #endif
619
620 2341 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
621 4682 msg.timestamp.time_since_epoch()) %
622 4682 1000;
623
624 2341 *file_stream_ << "[" << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S")
625 2341 << "." << std::setfill('0') << std::setw(3) << ms.count()
626 << std::setfill(' ') << "] "
627 2341 << "[" << std::setw(7) << std::left << log_level_to_string(msg.level) << "] :: "
628 2341 << msg.message() << '\n';
629 }
630
631 125 file_stream_->flush();
632
1/2
✓ Branch 65 → 66 taken 125 times.
✗ Branch 65 → 68 not taken.
125 }
633
634 1659 bool AsyncLogger::handle_overflow(LogMessage &&message) noexcept
635 {
636
4/5
✓ Branch 2 → 3 taken 1260 times.
✓ Branch 2 → 6 taken 207 times.
✓ Branch 2 → 23 taken 3 times.
✓ Branch 2 → 51 taken 193 times.
✗ Branch 2 → 105 not taken.
1659 switch (config_.overflow_policy)
637 {
638 1260 case OverflowPolicy::DropNewest:
639 1260 dropped_messages_.fetch_add(1, std::memory_order_relaxed);
640 1260 return false;
641
642 207 case OverflowPolicy::DropOldest:
643 {
644 207 LogMessage oldest;
645
1/2
✓ Branch 8 → 9 taken 207 times.
✗ Branch 8 → 18 not taken.
207 if (queue_.try_pop(oldest))
646 {
647 // Count the evicted oldest message as dropped
648 207 dropped_messages_.fetch_add(1, std::memory_order_relaxed);
649
1/2
✓ Branch 12 → 13 taken 207 times.
✗ Branch 12 → 15 not taken.
207 if (queue_.try_push(message))
650 {
651 // Net effect on pending_messages_: pop(-1) + push(+1) = 0
652 207 flush_cv_.notify_one();
653 207 return true;
654 }
655 // Pop succeeded but push failed: net -1
656 pending_messages_.fetch_sub(1, std::memory_order_acq_rel);
657 }
658 // Count the new message as dropped (separate from the evicted oldest above).
659 // dropped_messages_ counts individual lost messages, not overflow events.
660 dropped_messages_.fetch_add(1, std::memory_order_relaxed);
661 return false;
662 207 }
663
664 3 case OverflowPolicy::Block:
665 {
666 3 const auto deadline = std::chrono::steady_clock::now() + config_.block_timeout_ms;
667 3 size_t spin_count = 0;
668
669 // Pre-increment so flush sees the in-flight message throughout the retry loop
670 3 pending_messages_.fetch_add(1, std::memory_order_acq_rel);
671
672
1/2
✓ Branch 44 → 28 taken 1261 times.
✗ Branch 44 → 45 not taken.
1261 while (std::chrono::steady_clock::now() < deadline)
673 {
674
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 32 taken 1258 times.
1261 if (queue_.try_push(message))
675 {
676 3 flush_cv_.notify_one();
677 3 return true;
678 }
679
680
2/2
✓ Branch 32 → 33 taken 96 times.
✓ Branch 32 → 34 taken 1162 times.
1258 if (spin_count < config_.spin_backoff_iterations)
681 {
682 96 ++spin_count;
683 }
684
1/2
✓ Branch 34 → 35 taken 1162 times.
✗ Branch 34 → 37 not taken.
1162 else if (spin_count < config_.block_max_spin_iterations)
685 {
686 1162 std::this_thread::yield();
687 1162 ++spin_count;
688 }
689 else
690 {
691 std::this_thread::sleep_for(std::chrono::milliseconds(1));
692 }
693 }
694 // Timed out — undo the pre-increment
695 pending_messages_.fetch_sub(1, std::memory_order_acq_rel);
696 dropped_messages_.fetch_add(1, std::memory_order_relaxed);
697 return false;
698 }
699
700 193 case OverflowPolicy::SyncFallback:
701 {
702 193 std::lock_guard<std::mutex> lock(*log_mutex_);
703
3/6
✓ Branch 55 → 56 taken 193 times.
✗ Branch 55 → 59 not taken.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 60 taken 193 times.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 193 times.
193 if (!file_stream_->is_open() || !file_stream_->good())
704 {
705 return false;
706 }
707
708 193 const auto time_t = std::chrono::system_clock::to_time_t(message.timestamp);
709 193 std::tm tm_buf{};
710
711 #if defined(_WIN32) || defined(_MSC_VER)
712 193 localtime_s(&tm_buf, &time_t);
713 #else
714 localtime_r(&time_t, &tm_buf);
715 #endif
716
717 193 const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
718 386 message.timestamp.time_since_epoch()) %
719 386 1000;
720 193 *file_stream_ << "[" << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S")
721 193 << "." << std::setfill('0') << std::setw(3) << ms.count()
722 << std::setfill(' ') << "] "
723 193 << "[" << std::setw(7) << std::left << log_level_to_string(message.level) << "] :: "
724 193 << message.message() << '\n';
725 193 file_stream_->flush();
726
727
1/2
✗ Branch 100 → 101 not taken.
✓ Branch 100 → 102 taken 193 times.
193 if (file_stream_->fail())
728 {
729 return false;
730 }
731 193 return true;
732 193 }
733
734 default:
735 dropped_messages_.fetch_add(1, std::memory_order_relaxed);
736 return false;
737 }
738 }
739
740 } // namespace DetourModKit
741