GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 88.9% 16 / 0 / 18
Functions: 100.0% 5 / 0 / 5
Branches: 85.7% 12 / 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 class StringPool
50 {
51 public:
52 static StringPool &instance() noexcept;
53
54 [[nodiscard]] std::string *allocate(size_t size);
55 void deallocate(std::string *ptr) noexcept;
56
57 StringPool(const StringPool &) = delete;
58 StringPool &operator=(const StringPool &) = delete;
59 StringPool(StringPool &&) = delete;
60 StringPool &operator=(StringPool &&) = delete;
61
62 private:
63 struct PoolSlot
64 {
65 std::string str;
66 PoolSlot *next_free{nullptr};
67 };
68 #if defined(__GNUC__) || defined(__clang__)
69 #pragma GCC diagnostic push
70 #pragma GCC diagnostic ignored "-Winvalid-offsetof"
71 #endif
72 static_assert(offsetof(PoolSlot, str) == 0,
73 "PoolSlot::str must be the first member for pointer arithmetic in deallocate()");
74 #if defined(__GNUC__) || defined(__clang__)
75 #pragma GCC diagnostic pop
76 #endif
77
78 struct Block
79 {
80 alignas(64) char data[POOL_SLOTS_PER_BLOCK * sizeof(PoolSlot)];
81 Block *next{nullptr};
82 PoolSlot *free_list{nullptr};
83 size_t slot_count{0};
84 uint32_t constructed_mask{0};
85
86 PoolSlot *get_slot(size_t index) noexcept
87 {
88 return reinterpret_cast<PoolSlot *>(data) + index;
89 }
90 };
91
92 StringPool();
93 ~StringPool() noexcept;
94
95 /// Must be called with pool_mutex_ held.
96 void grow_pool_locked();
97 PoolSlot *claim_free_slot() noexcept;
98 void return_slot_locked(PoolSlot *slot, Block *block) noexcept;
99
100 std::atomic<Block *> head_{nullptr};
101 std::atomic<size_t> pool_size_{0};
102 std::atomic<size_t> heap_fallback_count_{0};
103 std::mutex pool_mutex_;
104 };
105
106 /**
107 * @struct LogMessage
108 * @brief A log entry with inline buffer optimization and overflow handling.
109 * @details Messages <= 512 bytes are stored inline. Larger messages use
110 * heap allocation via StringPool.
111 */
112 struct LogMessage
113 {
114 LogLevel level;
115 std::chrono::system_clock::time_point timestamp;
116 std::thread::id thread_id;
117
118 static constexpr size_t MAX_INLINE_SIZE = 512;
119 static constexpr size_t MAX_VALID_LENGTH = MAX_MESSAGE_SIZE;
120 std::array<char, MAX_INLINE_SIZE> buffer;
121 size_t length{0};
122
123 // Owned: allocated by StringPool, freed by reset().
124 std::string *overflow{nullptr};
125
126 LogMessage(LogLevel lvl, std::string_view msg);
127 338158 LogMessage() noexcept = default;
128
129 ~LogMessage() noexcept;
130
131 LogMessage(LogMessage &&other) noexcept;
132 LogMessage &operator=(LogMessage &&other) noexcept;
133
134 LogMessage(const LogMessage &) = delete;
135 LogMessage &operator=(const LogMessage &) = delete;
136
137 [[nodiscard]] std::string_view message() const noexcept;
138 [[nodiscard]] bool is_valid() const noexcept;
139 void reset() noexcept;
140 };
141
142 /**
143 * @class DynamicMPMCQueue
144 * @brief A dynamically-sized, bounded Multi-Producer Multi-Consumer queue.
145 * @details Uses a ring buffer with atomic sequence numbers for lock-free
146 * synchronization. Capacity is determined at construction time.
147 * @note This queue is designed to be constructed once and never resized.
148 * Moving slots after construction is not supported and will cause data corruption.
149 */
150 class DynamicMPMCQueue
151 {
152 public:
153 /**
154 * @brief Constructs a queue with the specified capacity.
155 * @param capacity The maximum number of elements (must be power of 2 and >= 2).
156 */
157 explicit DynamicMPMCQueue(size_t capacity);
158
159 71 ~DynamicMPMCQueue() = default;
160
161 DynamicMPMCQueue(const DynamicMPMCQueue &) = delete;
162 DynamicMPMCQueue &operator=(const DynamicMPMCQueue &) = delete;
163 DynamicMPMCQueue(DynamicMPMCQueue &&) = delete;
164 DynamicMPMCQueue &operator=(DynamicMPMCQueue &&) = delete;
165
166 /**
167 * @brief Attempts to push an item into the queue.
168 * @param item The item to push. Moved into the queue on success only;
169 * left unchanged on failure so the caller can retry or handle overflow.
170 * @return true if successful, false if queue is full.
171 */
172 bool try_push(LogMessage &item);
173
174 /**
175 * @brief Attempts to pop an item from the queue.
176 * @param item Reference to store the popped item.
177 * @return true if successful, false if queue is empty.
178 */
179 bool try_pop(LogMessage &item);
180
181 /**
182 * @brief Attempts to pop multiple items up to a maximum count.
183 * @param items Reference to a vector to store popped items.
184 * @param max_count Maximum number of items to pop.
185 * @return size_t Number of items actually popped.
186 */
187 size_t try_pop_batch(std::vector<LogMessage> &items, size_t max_count);
188
189 /// Returns the approximate number of items in the queue.
190 size_t size() const noexcept;
191
192 /// Checks if the queue is approximately empty.
193 bool empty() const noexcept;
194
195 /**
196 * @brief Returns the capacity of the queue.
197 * @return size_t The maximum number of elements.
198 */
199 1 size_t capacity() const noexcept { return capacity_; }
200
201 private:
202 struct Slot
203 {
204 std::atomic<size_t> sequence;
205 LogMessage data;
206
207 337762 Slot() noexcept : sequence(0) {}
208
209 Slot(const Slot &) = delete;
210 Slot &operator=(const Slot &) = delete;
211 Slot(Slot &&) = delete;
212 Slot &operator=(Slot &&) = delete;
213 };
214
215 /// Validates capacity before member initialization to prevent
216 /// allocation of an invalid-sized buffer in the initializer list.
217 static size_t validated_capacity(size_t capacity);
218
219 // Immutable after construction — never resized.
220 const size_t capacity_;
221 const size_t mask_;
222
223 // Allocated once in the constructor; the unique_ptr ensures immutability
224 // (no accidental resize) while maintaining contiguous cache-friendly layout.
225 std::unique_ptr<Slot[]> buffer_;
226
227 // Cache-line aligned to prevent false sharing between producers and consumers.
228 alignas(64) std::atomic<size_t> enqueue_pos_{0};
229 alignas(64) std::atomic<size_t> dequeue_pos_{0};
230 };
231
232 /**
233 * @struct AsyncLoggerConfig
234 * @brief Configuration for the async logger.
235 */
236 struct AsyncLoggerConfig
237 {
238 size_t queue_capacity = DEFAULT_QUEUE_CAPACITY;
239 size_t batch_size = DEFAULT_BATCH_SIZE;
240 std::chrono::milliseconds flush_interval = DEFAULT_FLUSH_INTERVAL;
241 OverflowPolicy overflow_policy = OverflowPolicy::DropOldest;
242 size_t spin_backoff_iterations = DEFAULT_SPIN_BACKOFF_ITERATIONS;
243 std::chrono::milliseconds block_timeout_ms{16};
244 size_t block_max_spin_iterations{1000};
245
246 66 [[nodiscard]] constexpr bool validate() const noexcept
247 {
248
4/4
✓ Branch 2 → 3 taken 64 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 63 times.
66 if (queue_capacity < 2 || (queue_capacity & (queue_capacity - 1)) != 0)
249 3 return false;
250
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 62 times.
63 if (batch_size == 0)
251 1 return false;
252
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 61 times.
62 if (flush_interval.count() <= 0)
253 1 return false;
254
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 60 times.
61 if (spin_backoff_iterations == 0)
255 1 return false;
256
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 60 times.
60 if (block_timeout_ms.count() <= 0)
257 return false;
258
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 60 times.
60 if (block_max_spin_iterations == 0)
259 return false;
260 60 return true;
261 }
262 };
263
264 // Compile-time validation: Default queue capacity must be a power of 2 and >= 2
265 static_assert(DEFAULT_QUEUE_CAPACITY >= 2 && (DEFAULT_QUEUE_CAPACITY & (DEFAULT_QUEUE_CAPACITY - 1)) == 0,
266 "DEFAULT_QUEUE_CAPACITY must be a power of 2 and at least 2");
267
268 /**
269 * @class AsyncLogger
270 * @brief Asynchronous logger that decouples log production from file I/O.
271 * @details Uses a lock-free queue to accept log messages from multiple threads
272 * and a dedicated writer thread to perform batched file writes.
273 * This significantly reduces latency on the producer side.
274 * @note Uses shared_ptr<WinFileStream> to safely handle Logger reconfiguration during runtime.
275 */
276 class AsyncLogger
277 {
278 public:
279 /**
280 * @brief Constructs an AsyncLogger with the given configuration.
281 * @param config The async logger configuration.
282 * @param file_stream Shared pointer to the output file stream (allows safe reconfigure).
283 * @param log_mutex Shared pointer to the mutex protecting the file stream.
284 */
285 explicit AsyncLogger(const AsyncLoggerConfig &config,
286 std::shared_ptr<WinFileStream> file_stream,
287 std::shared_ptr<std::mutex> log_mutex);
288
289 ~AsyncLogger() noexcept;
290
291 AsyncLogger(const AsyncLogger &) = delete;
292 AsyncLogger &operator=(const AsyncLogger &) = delete;
293 AsyncLogger(AsyncLogger &&) = delete;
294 AsyncLogger &operator=(AsyncLogger &&) = delete;
295
296 /**
297 * @brief Enqueues a log message for asynchronous writing.
298 * @param level The log level.
299 * @param message The message string.
300 * @return true if the message was successfully enqueued or written, false if dropped or timed out.
301 * @details This method is non-blocking (unless OverflowPolicy::Block is used).
302 * The message will be written to the log file by the writer thread.
303 */
304 [[nodiscard]] bool enqueue(LogLevel level, std::string_view message) noexcept;
305
306 /**
307 * @brief Flushes all pending log messages with a timeout.
308 * @param timeout Maximum time to wait for flush to complete.
309 * @return true if all messages were flushed, false if timeout occurred.
310 */
311 [[nodiscard]] bool flush_with_timeout(std::chrono::milliseconds timeout) noexcept;
312
313 /**
314 * @brief Flushes all pending log messages.
315 * @details Waits up to 500ms for all queued messages to be written.
316 * Uses a timeout to prevent indefinite blocking.
317 */
318 void flush() noexcept;
319
320 void shutdown() noexcept;
321
322 [[nodiscard]] bool is_running() const noexcept;
323
324 [[nodiscard]] size_t queue_size() const noexcept;
325
326 /**
327 * @brief Returns the total number of messages dropped due to queue overflow.
328 * @return size_t Number of dropped messages.
329 */
330 [[nodiscard]] size_t dropped_count() const noexcept;
331
332 /**
333 * @brief Resets the dropped message counter.
334 */
335 void reset_dropped_count() noexcept;
336
337 private:
338 void writer_thread_func() noexcept;
339
340 void write_batch(std::span<LogMessage> messages) noexcept;
341
342 bool handle_overflow(LogMessage &&message) noexcept;
343
344 DynamicMPMCQueue queue_;
345 AsyncLoggerConfig config_;
346
347 std::shared_ptr<WinFileStream> file_stream_;
348 std::shared_ptr<std::mutex> log_mutex_;
349
350 std::jthread writer_thread_;
351 std::atomic<bool> running_{false};
352 std::atomic<bool> shutdown_requested_{false};
353
354 std::mutex flush_mutex_;
355 std::condition_variable flush_cv_;
356 std::atomic<size_t> pending_messages_{0};
357 std::atomic<size_t> dropped_messages_{0};
358 };
359
360 } // namespace DetourModKit
361
362 #endif // DETOURMODKIT_ASYNC_LOGGER_HPP
363