GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 18 / 0 / 18
Functions: 100.0% 5 / 0 / 5
Branches: 100.0% 14 / 0 / 14

include/DetourModKit/async_logger.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_ASYNC_LOGGER_HPP
2 #define DETOURMODKIT_ASYNC_LOGGER_HPP
3
4 #include "DetourModKit/logger.hpp"
5
6 #include <array>
7 #include <atomic>
8 #include <cassert>
9 #include <chrono>
10 #include <condition_variable>
11 #include <cstddef>
12 #include <cstdint>
13 #include <memory>
14 #include <mutex>
15 #include <optional>
16 #include <span>
17 #include <string>
18 #include <string_view>
19 #include <thread>
20 #include <vector>
21
22 namespace DetourModKit
23 {
24 inline constexpr size_t DEFAULT_QUEUE_CAPACITY = 8192;
25 inline constexpr size_t DEFAULT_BATCH_SIZE = 64;
26 inline constexpr auto DEFAULT_FLUSH_INTERVAL = std::chrono::milliseconds(100);
27 inline constexpr size_t MAX_MESSAGE_SIZE = 16777216;
28 inline constexpr size_t DEFAULT_SPIN_BACKOFF_ITERATIONS = 32;
29 inline constexpr auto DEFAULT_FLUSH_TIMEOUT = std::chrono::milliseconds(500);
30 inline constexpr size_t MEMORY_POOL_BLOCK_SIZE = 4096;
31 inline constexpr size_t MEMORY_POOL_BLOCK_COUNT = 64;
32 inline constexpr size_t POOL_SLOTS_PER_BLOCK = 16;
33
34 enum class OverflowPolicy
35 {
36 DropNewest,
37 DropOldest,
38 Block,
39 SyncFallback
40 };
41
42 /**
43 * @class StringPool
44 * @brief Memory pool for small string allocations to reduce heap fragmentation.
45 * @details Uses a free-list approach for O(1) allocation/deallocation.
46 * Blocks are allocated on-demand up to MEMORY_POOL_BLOCK_COUNT.
47 * Each block is cache-line aligned to prevent false sharing.
48 *
49 * @note The singleton returned by instance() is intentionally leaked to
50 * avoid the static destruction order fiasco with late LogMessage
51 * teardown. Neither Bootstrap::request_shutdown() nor DMK_Shutdown()
52 * reclaim it; the OS releases the memory at process exit. The leak
53 * is bounded to MEMORY_POOL_BLOCK_COUNT blocks of
54 * MEMORY_POOL_BLOCK_SIZE bytes.
55 */
56 class StringPool
57 {
58 public:
59 static StringPool &instance() noexcept;
60
61 [[nodiscard]] std::string *allocate(size_t size);
62 void deallocate(std::string *ptr) noexcept;
63
64 StringPool(const StringPool &) = delete;
65 StringPool &operator=(const StringPool &) = delete;
66 StringPool(StringPool &&) = delete;
67 StringPool &operator=(StringPool &&) = delete;
68
69 private:
70 struct PoolSlot
71 {
72 std::string str;
73 PoolSlot *next_free{nullptr};
74 };
75 #if defined(__GNUC__) || defined(__clang__)
76 #pragma GCC diagnostic push
77 #pragma GCC diagnostic ignored "-Winvalid-offsetof"
78 #endif
79 static_assert(offsetof(PoolSlot, str) == 0,
80 "PoolSlot::str must be the first member for pointer arithmetic in deallocate()");
81 #if defined(__GNUC__) || defined(__clang__)
82 #pragma GCC diagnostic pop
83 #endif
84
85 struct Block
86 {
87 alignas(64) char data[POOL_SLOTS_PER_BLOCK * sizeof(PoolSlot)];
88 Block *next{nullptr};
89 PoolSlot *free_list{nullptr};
90 size_t slot_count{0};
91 uint32_t constructed_mask{0};
92
93 PoolSlot *get_slot(size_t index) noexcept
94 {
95 return reinterpret_cast<PoolSlot *>(data) + index;
96 }
97 };
98
99 StringPool();
100 ~StringPool() noexcept;
101
102 /// Must be called with pool_mutex_ held.
103 void grow_pool_locked();
104 PoolSlot *claim_free_slot() noexcept;
105 void return_slot_locked(PoolSlot *slot, Block *block) noexcept;
106
107 std::atomic<Block *> head_{nullptr};
108 std::atomic<size_t> pool_size_{0};
109 std::atomic<size_t> heap_fallback_count_{0};
110 std::mutex pool_mutex_;
111 };
112
113 /**
114 * @struct LogMessage
115 * @brief A log entry with inline buffer optimization and overflow handling.
116 * @details Messages <= 512 bytes are stored inline. Larger messages use
117 * heap allocation via StringPool.
118 */
119 struct LogMessage
120 {
121 LogLevel level;
122 std::chrono::system_clock::time_point timestamp;
123 std::thread::id thread_id;
124
125 static constexpr size_t MAX_INLINE_SIZE = 512;
126 static constexpr size_t MAX_VALID_LENGTH = MAX_MESSAGE_SIZE;
127 std::array<char, MAX_INLINE_SIZE> buffer;
128 size_t length{0};
129
130 // Owned: allocated by StringPool, freed by reset().
131 std::string *overflow{nullptr};
132
133 LogMessage(LogLevel lvl, std::string_view msg);
134 379193 LogMessage() noexcept = default;
135
136 ~LogMessage() noexcept;
137
138 LogMessage(LogMessage &&other) noexcept;
139 LogMessage &operator=(LogMessage &&other) noexcept;
140
141 LogMessage(const LogMessage &) = delete;
142 LogMessage &operator=(const LogMessage &) = delete;
143
144 [[nodiscard]] std::string_view message() const noexcept;
145 [[nodiscard]] bool is_valid() const noexcept;
146 void reset() noexcept;
147 };
148
149 /**
150 * @class DynamicMPMCQueue
151 * @brief A dynamically-sized, bounded Multi-Producer Multi-Consumer queue.
152 * @details Uses a ring buffer with atomic sequence numbers for lock-free
153 * synchronization. Capacity is determined at construction time.
154 * @note This queue is designed to be constructed once and never resized.
155 * Moving slots after construction is not supported and will cause data corruption.
156 */
157 #ifdef _MSC_VER
158 #pragma warning(push)
159 #pragma warning(disable : 4324) // structure was padded due to alignment specifier
160 #endif
161
162 class DynamicMPMCQueue
163 {
164 public:
165 /**
166 * @brief Constructs a queue with the specified capacity.
167 * @param capacity The maximum number of elements (must be power of 2 and >= 2).
168 */
169 explicit DynamicMPMCQueue(size_t capacity);
170
171 76 ~DynamicMPMCQueue() = default;
172
173 DynamicMPMCQueue(const DynamicMPMCQueue &) = delete;
174 DynamicMPMCQueue &operator=(const DynamicMPMCQueue &) = delete;
175 DynamicMPMCQueue(DynamicMPMCQueue &&) = delete;
176 DynamicMPMCQueue &operator=(DynamicMPMCQueue &&) = delete;
177
178 /**
179 * @brief Attempts to push an item into the queue.
180 * @param item The item to push. Moved into the queue on success only;
181 * left unchanged on failure so the caller can retry or handle overflow.
182 * @return true if successful, false if queue is full.
183 */
184 bool try_push(LogMessage &item);
185
186 /**
187 * @brief Attempts to pop an item from the queue.
188 * @param item Reference to store the popped item.
189 * @return true if successful, false if queue is empty.
190 */
191 bool try_pop(LogMessage &item);
192
193 /**
194 * @brief Attempts to pop multiple items up to a maximum count.
195 * @param items Reference to a vector to store popped items.
196 * @param max_count Maximum number of items to pop.
197 * @return size_t Number of items actually popped.
198 */
199 size_t try_pop_batch(std::vector<LogMessage> &items, size_t max_count);
200
201 /// Returns the approximate number of items in the queue.
202 size_t size() const noexcept;
203
204 /// Checks if the queue is approximately empty.
205 bool empty() const noexcept;
206
207 /**
208 * @brief Returns the capacity of the queue.
209 * @return size_t The maximum number of elements.
210 */
211 1 size_t capacity() const noexcept { return capacity_; }
212
213 private:
214 struct Slot
215 {
216 std::atomic<size_t> sequence;
217 LogMessage data;
218
219 378722 Slot() noexcept : sequence(0) {}
220
221 Slot(const Slot &) = delete;
222 Slot &operator=(const Slot &) = delete;
223 Slot(Slot &&) = delete;
224 Slot &operator=(Slot &&) = delete;
225 };
226
227 /// Validates capacity before member initialization to prevent
228 /// allocation of an invalid-sized buffer in the initializer list.
229 static size_t validated_capacity(size_t capacity);
230
231 // Immutable after construction — never resized.
232 const size_t capacity_;
233 const size_t mask_;
234
235 // Allocated once in the constructor; the unique_ptr ensures immutability
236 // (no accidental resize) while maintaining contiguous cache-friendly layout.
237 std::unique_ptr<Slot[]> buffer_;
238
239 // Cache-line aligned to prevent false sharing between producers and consumers.
240 alignas(64) std::atomic<size_t> enqueue_pos_{0};
241 alignas(64) std::atomic<size_t> dequeue_pos_{0};
242 };
243
244 #ifdef _MSC_VER
245 #pragma warning(pop)
246 #endif
247
248 /**
249 * @struct AsyncLoggerConfig
250 * @brief Configuration for the async logger.
251 */
252 struct AsyncLoggerConfig
253 {
254 size_t queue_capacity = DEFAULT_QUEUE_CAPACITY;
255 size_t batch_size = DEFAULT_BATCH_SIZE;
256 std::chrono::milliseconds flush_interval = DEFAULT_FLUSH_INTERVAL;
257 OverflowPolicy overflow_policy = OverflowPolicy::DropOldest;
258 size_t spin_backoff_iterations = DEFAULT_SPIN_BACKOFF_ITERATIONS;
259 std::chrono::milliseconds block_timeout_ms{16};
260 size_t block_max_spin_iterations{1000};
261
262 75 [[nodiscard]] constexpr bool validate() const noexcept
263 {
264
4/4
✓ Branch 2 → 3 taken 73 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 72 times.
75 if (queue_capacity < 2 || (queue_capacity & (queue_capacity - 1)) != 0)
265 3 return false;
266
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 71 times.
72 if (batch_size == 0)
267 1 return false;
268
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 69 times.
71 if (flush_interval.count() <= 0)
269 2 return false;
270
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 68 times.
69 if (spin_backoff_iterations == 0)
271 1 return false;
272
2/2
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 66 times.
68 if (block_timeout_ms.count() <= 0)
273 2 return false;
274
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 65 times.
66 if (block_max_spin_iterations == 0)
275 1 return false;
276 65 return true;
277 }
278 };
279
280 // Compile-time validation: Default queue capacity must be a power of 2 and >= 2
281 static_assert(DEFAULT_QUEUE_CAPACITY >= 2 && (DEFAULT_QUEUE_CAPACITY & (DEFAULT_QUEUE_CAPACITY - 1)) == 0,
282 "DEFAULT_QUEUE_CAPACITY must be a power of 2 and at least 2");
283
284 /**
285 * @class AsyncLogger
286 * @brief Asynchronous logger that decouples log production from file I/O.
287 * @details Uses a lock-free queue to accept log messages from multiple threads
288 * and a dedicated writer thread to perform batched file writes.
289 * This significantly reduces latency on the producer side.
290 * @note Uses shared_ptr<WinFileStream> to safely handle Logger reconfiguration during runtime.
291 */
292 class AsyncLogger
293 {
294 public:
295 /**
296 * @brief Constructs an AsyncLogger with the given configuration.
297 * @param config The async logger configuration.
298 * @param file_stream Shared pointer to the output file stream (allows safe reconfigure).
299 * @param log_mutex Shared pointer to the mutex protecting the file stream.
300 */
301 explicit AsyncLogger(const AsyncLoggerConfig &config,
302 std::shared_ptr<WinFileStream> file_stream,
303 std::shared_ptr<std::mutex> log_mutex);
304
305 ~AsyncLogger() noexcept;
306
307 AsyncLogger(const AsyncLogger &) = delete;
308 AsyncLogger &operator=(const AsyncLogger &) = delete;
309 AsyncLogger(AsyncLogger &&) = delete;
310 AsyncLogger &operator=(AsyncLogger &&) = delete;
311
312 /**
313 * @brief Enqueues a log message for asynchronous writing.
314 * @param level The log level.
315 * @param message The message string.
316 * @return true if the message was successfully enqueued or written, false if dropped or timed out.
317 * @details This method is non-blocking (unless OverflowPolicy::Block is used).
318 * The message will be written to the log file by the writer thread.
319 */
320 [[nodiscard]] bool enqueue(LogLevel level, std::string_view message) noexcept;
321
322 /**
323 * @brief Flushes all pending log messages with a timeout.
324 * @param timeout Maximum time to wait for flush to complete.
325 * @return true if all messages were flushed, false if timeout occurred.
326 */
327 [[nodiscard]] bool flush_with_timeout(std::chrono::milliseconds timeout) noexcept;
328
329 /**
330 * @brief Flushes all pending log messages.
331 * @details Waits up to 500ms for all queued messages to be written.
332 * Uses a timeout to prevent indefinite blocking.
333 */
334 void flush() noexcept;
335
336 /**
337 * @brief Stops the writer thread and drains remaining queued messages.
338 * @details Sets shutdown_requested_, joins the writer thread, then drains
339 * any messages that arrived between the stop signal and thread exit.
340 * @note A producer that already passed the shutdown_requested_ check but has
341 * not yet completed try_push() can enqueue at most one message after the
342 * final drain. This is an accepted trade-off to avoid adding atomic
343 * overhead (producers_in_flight counter) to every enqueue() call.
344 */
345 void shutdown() noexcept;
346
347 [[nodiscard]] bool is_running() const noexcept;
348
349 [[nodiscard]] size_t queue_size() const noexcept;
350
351 /**
352 * @brief Returns the total number of messages dropped due to queue overflow.
353 * @return size_t Number of dropped messages.
354 */
355 [[nodiscard]] size_t dropped_count() const noexcept;
356
357 /**
358 * @brief Resets the dropped message counter.
359 */
360 void reset_dropped_count() noexcept;
361
362 private:
363 void writer_thread_func() noexcept;
364
365 /**
366 * @brief Drains any messages remaining in the queue after the writer thread exits.
367 * @details Called during shutdown to flush late-enqueued messages that arrived
368 * between running_=false and the writer thread observing an empty queue.
369 * No external lock is required; the writer thread has already been joined.
370 */
371 void drain_remaining() noexcept;
372
373 void write_batch(std::span<LogMessage> messages) noexcept;
374
375 bool handle_overflow(LogMessage &&message) noexcept;
376
377 DynamicMPMCQueue queue_;
378 AsyncLoggerConfig config_;
379
380 std::shared_ptr<WinFileStream> file_stream_;
381 std::shared_ptr<std::mutex> log_mutex_;
382
383 std::jthread writer_thread_;
384 std::atomic<bool> running_{false};
385 std::atomic<bool> shutdown_requested_{false};
386
387 std::mutex flush_mutex_;
388 std::condition_variable flush_cv_;
389 std::atomic<size_t> pending_messages_{0};
390 std::atomic<size_t> dropped_messages_{0};
391 };
392
393 } // namespace DetourModKit
394
395 #endif // DETOURMODKIT_ASYNC_LOGGER_HPP
396