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 <chrono> | ||
| 9 | #include <condition_variable> | ||
| 10 | #include <cstddef> | ||
| 11 | #include <cstdint> | ||
| 12 | #include <memory> | ||
| 13 | #include <mutex> | ||
| 14 | #include <span> | ||
| 15 | #include <string> | ||
| 16 | #include <string_view> | ||
| 17 | #include <thread> | ||
| 18 | #include <vector> | ||
| 19 | |||
| 20 | namespace DetourModKit | ||
| 21 | { | ||
| 22 | /// Default capacity (slot count) of the bounded MPMC message queue. | ||
| 23 | inline constexpr size_t DEFAULT_QUEUE_CAPACITY = 8192; | ||
| 24 | /// Default number of messages the writer drains per write batch. | ||
| 25 | inline constexpr size_t DEFAULT_BATCH_SIZE = 64; | ||
| 26 | /// Default interval between periodic writer flushes. | ||
| 27 | inline constexpr auto DEFAULT_FLUSH_INTERVAL = std::chrono::milliseconds(100); | ||
| 28 | /// Maximum length, in bytes, of a single log message. | ||
| 29 | inline constexpr size_t MAX_MESSAGE_SIZE = 16777216; | ||
| 30 | /// Default spin-backoff iteration count before a producer yields/parks. | ||
| 31 | inline constexpr size_t DEFAULT_SPIN_BACKOFF_ITERATIONS = 32; | ||
| 32 | /// Default timeout for a blocking flush to complete. | ||
| 33 | inline constexpr auto DEFAULT_FLUSH_TIMEOUT = std::chrono::milliseconds(500); | ||
| 34 | /// Byte size of one StringPool overflow block. | ||
| 35 | inline constexpr size_t MEMORY_POOL_BLOCK_SIZE = 4096; | ||
| 36 | /// Number of overflow blocks the StringPool preallocates. | ||
| 37 | inline constexpr size_t MEMORY_POOL_BLOCK_COUNT = 64; | ||
| 38 | /// Number of allocation slots carved from each StringPool block. | ||
| 39 | inline constexpr size_t POOL_SLOTS_PER_BLOCK = 16; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * @enum OverflowPolicy | ||
| 43 | * @brief Action taken by AsyncLogger::enqueue when the bounded queue is full. | ||
| 44 | * @warning Only DropNewest and DropOldest are callback-safe. Block parks the producer and SyncFallback writes | ||
| 45 | * synchronously, so neither may be used by a logger that hook or input callbacks emit through; reserve | ||
| 46 | * them for setup/control-plane logging where a brief stall under sink backpressure is acceptable. | ||
| 47 | */ | ||
| 48 | enum class OverflowPolicy | ||
| 49 | { | ||
| 50 | /// Drop the message being enqueued when the queue is full. Non-blocking and callback-safe. | ||
| 51 | DropNewest, | ||
| 52 | /// Evict the oldest queued message to make room for the new one. Non-blocking and callback-safe. | ||
| 53 | DropOldest, | ||
| 54 | /// Park the producer until space frees or block_timeout_ms elapses. Not callback-safe (can stall the caller). | ||
| 55 | Block, | ||
| 56 | /// Write the message synchronously on the producer thread. Not callback-safe (synchronous sink I/O). | ||
| 57 | SyncFallback | ||
| 58 | }; | ||
| 59 | |||
| 60 | /** | ||
| 61 | * @class StringPool | ||
| 62 | * @brief Memory pool for small string allocations to reduce heap fragmentation. | ||
| 63 | * @details Uses a free-list approach for O(1) allocation/deallocation. Blocks are allocated on-demand up to | ||
| 64 | * MEMORY_POOL_BLOCK_COUNT. Each block is cache-line aligned to prevent false sharing. | ||
| 65 | * | ||
| 66 | * @note The singleton returned by instance() is intentionally leaked to avoid the static destruction order fiasco | ||
| 67 | * with late LogMessage teardown. Neither Bootstrap::request_shutdown() nor DMK_Shutdown() reclaim it; the OS | ||
| 68 | * releases the memory at process exit. The leak is bounded to MEMORY_POOL_BLOCK_COUNT blocks of | ||
| 69 | * MEMORY_POOL_BLOCK_SIZE bytes. | ||
| 70 | */ | ||
| 71 | class StringPool | ||
| 72 | { | ||
| 73 | public: | ||
| 74 | static StringPool &instance() noexcept; | ||
| 75 | |||
| 76 | [[nodiscard]] std::string *allocate(size_t size) noexcept; | ||
| 77 | void deallocate(std::string *ptr) noexcept; | ||
| 78 | |||
| 79 | StringPool(const StringPool &) = delete; | ||
| 80 | StringPool &operator=(const StringPool &) = delete; | ||
| 81 | StringPool(StringPool &&) = delete; | ||
| 82 | StringPool &operator=(StringPool &&) = delete; | ||
| 83 | |||
| 84 | private: | ||
| 85 | struct PoolSlot | ||
| 86 | { | ||
| 87 | std::string str; | ||
| 88 | PoolSlot *next_free{nullptr}; | ||
| 89 | }; | ||
| 90 | #if defined(__GNUC__) || defined(__clang__) | ||
| 91 | #pragma GCC diagnostic push | ||
| 92 | #pragma GCC diagnostic ignored "-Winvalid-offsetof" | ||
| 93 | #endif | ||
| 94 | static_assert(offsetof(PoolSlot, str) == 0, | ||
| 95 | "PoolSlot::str must be the first member for pointer arithmetic in deallocate()"); | ||
| 96 | #if defined(__GNUC__) || defined(__clang__) | ||
| 97 | #pragma GCC diagnostic pop | ||
| 98 | #endif | ||
| 99 | |||
| 100 | struct Block | ||
| 101 | { | ||
| 102 | alignas(64) char data[POOL_SLOTS_PER_BLOCK * sizeof(PoolSlot)]; | ||
| 103 | Block *next{nullptr}; | ||
| 104 | PoolSlot *free_list{nullptr}; | ||
| 105 | uint32_t constructed_mask{0}; | ||
| 106 | }; | ||
| 107 | |||
| 108 | StringPool() noexcept; | ||
| 109 | ~StringPool() noexcept; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * @brief Appends one block to the pool. Must be called with m_pool_mutex held. | ||
| 113 | * @details No-throw: an allocation failure leaves the pool unchanged so | ||
| 114 | * callers can fall back to a nothrow heap string instead of throwing out of the logging path. | ||
| 115 | */ | ||
| 116 | void grow_pool_locked() noexcept; | ||
| 117 | PoolSlot *claim_free_slot() noexcept; | ||
| 118 | void return_slot_locked(PoolSlot *slot, Block *block) noexcept; | ||
| 119 | |||
| 120 | std::atomic<Block *> m_head{nullptr}; | ||
| 121 | std::atomic<size_t> m_heap_fallback_count{0}; | ||
| 122 | std::mutex m_pool_mutex; | ||
| 123 | }; | ||
| 124 | |||
| 125 | /** | ||
| 126 | * @struct LogMessage | ||
| 127 | * @brief A log entry with inline buffer optimization and overflow handling. | ||
| 128 | * @details Messages <= 512 bytes are stored inline. Larger messages use heap allocation via StringPool. This is a | ||
| 129 | * move-only transport/value type (copy deleted, move defined) carried by value through the queue; it | ||
| 130 | * keeps plain field names rather than the m_ member prefix, per the POD-struct naming convention, even | ||
| 131 | * though @ref overflow is an owned heap pointer -- its lifetime is self-contained, released by reset() | ||
| 132 | * and the destructor, so no class invariant rides on member encapsulation. | ||
| 133 | */ | ||
| 134 | struct LogMessage | ||
| 135 | { | ||
| 136 | LogLevel level{LogLevel::Info}; | ||
| 137 | std::chrono::system_clock::time_point timestamp; | ||
| 138 | std::thread::id thread_id; | ||
| 139 | |||
| 140 | static constexpr size_t MAX_INLINE_SIZE = LOG_INLINE_MESSAGE_SIZE; | ||
| 141 | static constexpr size_t MAX_VALID_LENGTH = MAX_MESSAGE_SIZE; | ||
| 142 | // Left uninitialized (raw storage): only [0, length) is ever read, and every constructor/move writes exactly | ||
| 143 | // length bytes before any read. Zero-filling the whole inline buffer would memset MAX_INLINE_SIZE bytes on each | ||
| 144 | // construction/enqueue for no observable effect. | ||
| 145 | std::array<char, MAX_INLINE_SIZE> buffer; | ||
| 146 | size_t length{0}; | ||
| 147 | |||
| 148 | // Owned: allocated by StringPool, freed by reset(). | ||
| 149 | std::string *overflow{nullptr}; | ||
| 150 | |||
| 151 | LogMessage(LogLevel lvl, std::string_view msg) noexcept; | ||
| 152 | 425761 | LogMessage() noexcept = default; | |
| 153 | |||
| 154 | ~LogMessage() noexcept; | ||
| 155 | |||
| 156 | LogMessage(LogMessage &&other) noexcept; | ||
| 157 | LogMessage &operator=(LogMessage &&other) noexcept; | ||
| 158 | |||
| 159 | LogMessage(const LogMessage &) = delete; | ||
| 160 | LogMessage &operator=(const LogMessage &) = delete; | ||
| 161 | |||
| 162 | [[nodiscard]] std::string_view message() const noexcept; | ||
| 163 | [[nodiscard]] bool is_valid() const noexcept; | ||
| 164 | void reset() noexcept; | ||
| 165 | }; | ||
| 166 | |||
| 167 | /** | ||
| 168 | * @class DynamicMPMCQueue | ||
| 169 | * @brief A dynamically-sized, bounded Multi-Producer Multi-Consumer queue. | ||
| 170 | * @details Uses a ring buffer with atomic sequence numbers for lock-free synchronization. Capacity is determined at | ||
| 171 | * construction time. | ||
| 172 | * @note This queue is designed to be constructed once and never resized. Moving slots after construction is not | ||
| 173 | * supported and will cause data corruption. | ||
| 174 | */ | ||
| 175 | #ifdef _MSC_VER | ||
| 176 | #pragma warning(push) | ||
| 177 | // structure was padded due to alignment specifier | ||
| 178 | #pragma warning(disable : 4324) | ||
| 179 | #endif | ||
| 180 | |||
| 181 | class DynamicMPMCQueue | ||
| 182 | { | ||
| 183 | public: | ||
| 184 | /** | ||
| 185 | * @brief Constructs a queue with the specified capacity. | ||
| 186 | * @param capacity The maximum number of elements (must be power of 2 and >= 2). | ||
| 187 | */ | ||
| 188 | explicit DynamicMPMCQueue(size_t capacity); | ||
| 189 | |||
| 190 | 82 | ~DynamicMPMCQueue() noexcept = default; | |
| 191 | |||
| 192 | DynamicMPMCQueue(const DynamicMPMCQueue &) = delete; | ||
| 193 | DynamicMPMCQueue &operator=(const DynamicMPMCQueue &) = delete; | ||
| 194 | DynamicMPMCQueue(DynamicMPMCQueue &&) = delete; | ||
| 195 | DynamicMPMCQueue &operator=(DynamicMPMCQueue &&) = delete; | ||
| 196 | |||
| 197 | /** | ||
| 198 | * @brief Attempts to push an item into the queue. | ||
| 199 | * @param item The item to push. Moved into the queue on success only; | ||
| 200 | * left unchanged on failure so the caller can retry or handle overflow. | ||
| 201 | * @return true if successful, false if queue is full. | ||
| 202 | */ | ||
| 203 | [[nodiscard]] bool try_push(LogMessage &item); | ||
| 204 | |||
| 205 | /** | ||
| 206 | * @brief Attempts to pop an item from the queue. | ||
| 207 | * @param item Reference to store the popped item. | ||
| 208 | * @return true if successful, false if queue is empty. | ||
| 209 | */ | ||
| 210 | [[nodiscard]] bool try_pop(LogMessage &item); | ||
| 211 | |||
| 212 | /** | ||
| 213 | * @brief Attempts to pop multiple items up to a maximum count. | ||
| 214 | * @param items Reference to a vector to store popped items. | ||
| 215 | * @param max_count Maximum number of items to pop. | ||
| 216 | * @return size_t Number of items actually popped. | ||
| 217 | */ | ||
| 218 | [[nodiscard]] size_t try_pop_batch(std::vector<LogMessage> &items, size_t max_count); | ||
| 219 | |||
| 220 | /// Returns the approximate number of items in the queue. | ||
| 221 | [[nodiscard]] size_t size() const noexcept; | ||
| 222 | |||
| 223 | /// Checks if the queue is approximately empty. | ||
| 224 | [[nodiscard]] bool empty() const noexcept; | ||
| 225 | |||
| 226 | /** | ||
| 227 | * @brief Returns the capacity of the queue. | ||
| 228 | * @return size_t The maximum number of elements. | ||
| 229 | */ | ||
| 230 | 1 | [[nodiscard]] size_t capacity() const noexcept { return m_capacity; } | |
| 231 | |||
| 232 | private: | ||
| 233 | struct Slot | ||
| 234 | { | ||
| 235 | std::atomic<size_t> sequence; | ||
| 236 | LogMessage data; | ||
| 237 | |||
| 238 | 412578 | Slot() noexcept : sequence(0) {} | |
| 239 | |||
| 240 | Slot(const Slot &) = delete; | ||
| 241 | Slot &operator=(const Slot &) = delete; | ||
| 242 | Slot(Slot &&) = delete; | ||
| 243 | Slot &operator=(Slot &&) = delete; | ||
| 244 | }; | ||
| 245 | |||
| 246 | /** | ||
| 247 | * @brief Validates capacity before member initialization to prevent allocation of an invalid-sized buffer in | ||
| 248 | * the initializer list. | ||
| 249 | */ | ||
| 250 | static size_t validated_capacity(size_t capacity); | ||
| 251 | |||
| 252 | // Immutable after construction -- never resized. | ||
| 253 | const size_t m_capacity; | ||
| 254 | const size_t m_mask; | ||
| 255 | |||
| 256 | // Allocated once in the constructor; the unique_ptr ensures immutability (no accidental resize) while | ||
| 257 | // maintaining contiguous cache-friendly layout. | ||
| 258 | std::unique_ptr<Slot[]> m_buffer; | ||
| 259 | |||
| 260 | // Cache-line aligned to prevent false sharing between producers and consumers. | ||
| 261 | alignas(64) std::atomic<size_t> m_enqueue_pos{0}; | ||
| 262 | alignas(64) std::atomic<size_t> m_dequeue_pos{0}; | ||
| 263 | }; | ||
| 264 | |||
| 265 | #ifdef _MSC_VER | ||
| 266 | #pragma warning(pop) | ||
| 267 | #endif | ||
| 268 | |||
| 269 | /** | ||
| 270 | * @struct AsyncLoggerConfig | ||
| 271 | * @brief Configuration for the async logger. | ||
| 272 | * @details The default queue holds DEFAULT_QUEUE_CAPACITY (8192) slots; each slot embeds a LogMessage with a | ||
| 273 | * LOG_INLINE_MESSAGE_SIZE (512) byte inline buffer, so the ring buffer's resident footprint is on the | ||
| 274 | * order of a few MiB at the default capacity (queue_capacity must stay a power of two). The StringPool | ||
| 275 | * used for overflow (> 512 byte) messages is a separate, lazily grown allocation bounded to | ||
| 276 | * MEMORY_POOL_BLOCK_COUNT * MEMORY_POOL_BLOCK_SIZE (256 KiB); see StringPool. Shrink queue_capacity for | ||
| 277 | * memory-constrained hosts. | ||
| 278 | */ | ||
| 279 | struct AsyncLoggerConfig | ||
| 280 | { | ||
| 281 | size_t queue_capacity = DEFAULT_QUEUE_CAPACITY; | ||
| 282 | size_t batch_size = DEFAULT_BATCH_SIZE; | ||
| 283 | std::chrono::milliseconds flush_interval = DEFAULT_FLUSH_INTERVAL; | ||
| 284 | OverflowPolicy overflow_policy = OverflowPolicy::DropOldest; | ||
| 285 | size_t spin_backoff_iterations = DEFAULT_SPIN_BACKOFF_ITERATIONS; | ||
| 286 | std::chrono::milliseconds block_timeout_ms{16}; | ||
| 287 | size_t block_max_spin_iterations{1000}; | ||
| 288 | /** | ||
| 289 | * @brief strftime-style date/time format for the async sink. | ||
| 290 | * @details Kept in sync with the synchronous Logger by Logger::enable_async_mode so both sinks emit identical | ||
| 291 | * timestamps; the trailing ".<ms>" is appended by the writer, not this format. | ||
| 292 | */ | ||
| 293 | std::string timestamp_format{"%Y-%m-%d %H:%M:%S"}; | ||
| 294 | |||
| 295 | 81 | [[nodiscard]] constexpr bool validate() const noexcept | |
| 296 | { | ||
| 297 |
4/4✓ Branch 2 → 3 taken 79 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 78 times.
|
81 | if (queue_capacity < 2 || (queue_capacity & (queue_capacity - 1)) != 0) |
| 298 | 3 | return false; | |
| 299 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 77 times.
|
78 | if (batch_size == 0) |
| 300 | 1 | return false; | |
| 301 |
2/2✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 75 times.
|
77 | if (flush_interval.count() <= 0) |
| 302 | 2 | return false; | |
| 303 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 74 times.
|
75 | if (spin_backoff_iterations == 0) |
| 304 | 1 | return false; | |
| 305 |
2/2✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 72 times.
|
74 | if (block_timeout_ms.count() <= 0) |
| 306 | 2 | return false; | |
| 307 |
2/2✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 71 times.
|
72 | if (block_max_spin_iterations == 0) |
| 308 | 1 | return false; | |
| 309 | 71 | return true; | |
| 310 | } | ||
| 311 | }; | ||
| 312 | |||
| 313 | // Compile-time validation: Default queue capacity must be a power of 2 and >= 2 | ||
| 314 | static_assert(DEFAULT_QUEUE_CAPACITY >= 2 && (DEFAULT_QUEUE_CAPACITY & (DEFAULT_QUEUE_CAPACITY - 1)) == 0, | ||
| 315 | "DEFAULT_QUEUE_CAPACITY must be a power of 2 and at least 2"); | ||
| 316 | |||
| 317 | /** | ||
| 318 | * @class AsyncLogger | ||
| 319 | * @brief Asynchronous logger that decouples log production from file I/O. | ||
| 320 | * @details Uses a lock-free queue to accept log messages from multiple threads and a dedicated writer thread to | ||
| 321 | * perform batched file writes. This significantly reduces latency on the producer side. | ||
| 322 | * @note Uses shared_ptr<WinFileStream> to safely handle Logger reconfiguration during runtime. | ||
| 323 | */ | ||
| 324 | class AsyncLogger | ||
| 325 | { | ||
| 326 | public: | ||
| 327 | /** | ||
| 328 | * @brief Constructs an AsyncLogger with the given configuration. | ||
| 329 | * @param config The async logger configuration. | ||
| 330 | * @param file_stream Shared pointer to the output file stream (allows safe reconfigure). | ||
| 331 | * @param log_mutex Shared pointer to the mutex protecting the file stream. | ||
| 332 | */ | ||
| 333 | explicit AsyncLogger(const AsyncLoggerConfig &config, std::shared_ptr<WinFileStream> file_stream, | ||
| 334 | std::shared_ptr<std::mutex> log_mutex); | ||
| 335 | |||
| 336 | ~AsyncLogger() noexcept; | ||
| 337 | |||
| 338 | AsyncLogger(const AsyncLogger &) = delete; | ||
| 339 | AsyncLogger &operator=(const AsyncLogger &) = delete; | ||
| 340 | AsyncLogger(AsyncLogger &&) = delete; | ||
| 341 | AsyncLogger &operator=(AsyncLogger &&) = delete; | ||
| 342 | |||
| 343 | /** | ||
| 344 | * @brief Enqueues a log message for asynchronous writing. | ||
| 345 | * @param level The log level. | ||
| 346 | * @param message The message string. | ||
| 347 | * @return true if the message was successfully enqueued or written, false if dropped or timed out. | ||
| 348 | * @details Non-blocking under the DropNewest / DropOldest policies. Under OverflowPolicy::Block a full queue | ||
| 349 | * parks the caller up to block_timeout_ms, and under OverflowPolicy::SyncFallback a full queue writes | ||
| 350 | * the message synchronously on the calling thread, so neither of those policies is callback-safe (see | ||
| 351 | * OverflowPolicy). Otherwise the message is written by the writer thread. | ||
| 352 | * @note Best-effort: never throws and returns false on drop. Callback-safe only under the DropNewest / | ||
| 353 | * DropOldest policies (see @details). | ||
| 354 | */ | ||
| 355 | [[nodiscard]] bool enqueue(LogLevel level, std::string_view message) noexcept; | ||
| 356 | |||
| 357 | /** | ||
| 358 | * @brief Flushes all pending log messages with a timeout. | ||
| 359 | * @param timeout Maximum time to wait for flush to complete. | ||
| 360 | * @return true if all messages were flushed, false if timeout occurred. | ||
| 361 | */ | ||
| 362 | [[nodiscard]] bool flush_with_timeout(std::chrono::milliseconds timeout) noexcept; | ||
| 363 | |||
| 364 | /** | ||
| 365 | * @brief Flushes all pending log messages. | ||
| 366 | * @details Waits up to 500ms for all queued messages to be written. Uses a timeout to prevent indefinite | ||
| 367 | * blocking. | ||
| 368 | */ | ||
| 369 | void flush() noexcept; | ||
| 370 | |||
| 371 | /** | ||
| 372 | * @brief Stops the writer thread and drains remaining queued messages. | ||
| 373 | * @details Sets m_shutdown_requested, joins the writer thread, then drains any messages that arrived between | ||
| 374 | * the stop signal and thread exit. | ||
| 375 | * @note A producer that already passed the m_shutdown_requested check but has not yet completed try_push() can | ||
| 376 | * enqueue at most one message after the final drain. This is an accepted trade-off to avoid adding atomic | ||
| 377 | * overhead (producers_in_flight counter) to every enqueue() call. | ||
| 378 | */ | ||
| 379 | void shutdown() noexcept; | ||
| 380 | |||
| 381 | [[nodiscard]] bool is_running() const noexcept; | ||
| 382 | |||
| 383 | /** | ||
| 384 | * @brief Reports whether the writer thread is currently parked on the flush condition variable. | ||
| 385 | * @details Observability accessor for the idle-park state set by the writer immediately before it | ||
| 386 | * blocks in wait_for and cleared when it wakes. Lets a test or diagnostic confirm the | ||
| 387 | * writer has reached the parked path deterministically instead of relying on a fixed | ||
| 388 | * sleep. The flag can flip at any time, so treat the result as a point-in-time snapshot. | ||
| 389 | */ | ||
| 390 | [[nodiscard]] bool is_writer_waiting() const noexcept; | ||
| 391 | |||
| 392 | [[nodiscard]] size_t queue_size() const noexcept; | ||
| 393 | |||
| 394 | /** | ||
| 395 | * @brief Returns the total number of messages dropped due to queue overflow. | ||
| 396 | * @return size_t Number of dropped messages. | ||
| 397 | */ | ||
| 398 | [[nodiscard]] size_t dropped_count() const noexcept; | ||
| 399 | |||
| 400 | /** | ||
| 401 | * @brief Resets the dropped message counter. | ||
| 402 | */ | ||
| 403 | void reset_dropped_count() noexcept; | ||
| 404 | |||
| 405 | private: | ||
| 406 | void writer_thread_func() noexcept; | ||
| 407 | |||
| 408 | /** | ||
| 409 | * @brief Drains any messages remaining in the queue after the writer thread exits. | ||
| 410 | * @details Called during shutdown to flush late-enqueued messages that arrived between m_running=false and the | ||
| 411 | * writer thread observing an empty queue. No external lock is required; the writer thread has already | ||
| 412 | * been joined. | ||
| 413 | */ | ||
| 414 | void drain_remaining() noexcept; | ||
| 415 | |||
| 416 | void write_batch(std::span<LogMessage> messages) noexcept; | ||
| 417 | |||
| 418 | bool handle_overflow(LogMessage &&message) noexcept; | ||
| 419 | |||
| 420 | /** | ||
| 421 | * @brief Wakes the writer thread if it is parked on m_flush_cv after a successful push. | ||
| 422 | * @details A successful producer has already made m_pending_messages non-zero before publishing | ||
| 423 | * a new queue slot, or preserved it while replacing an old slot. The writer publishes | ||
| 424 | * m_writer_waiting before checking that same pending count and blocking. Those seq_cst | ||
| 425 | * operations form a store/load handshake: if the writer misses the pending state, the | ||
| 426 | * producer observes m_writer_waiting and notifies under m_flush_mutex; if the producer | ||
| 427 | * observes false, the writer's pending-count predicate sees the work and does not block. | ||
| 428 | * notify_all (not notify_one) is used so a flusher waiting on the same condition variable | ||
| 429 | * cannot absorb the notification and leave the writer asleep. When the writer is | ||
| 430 | * draining, this is a seq_cst flag load with no mutex or syscall. | ||
| 431 | */ | ||
| 432 | void notify_writer() noexcept; | ||
| 433 | |||
| 434 | DynamicMPMCQueue m_queue; | ||
| 435 | AsyncLoggerConfig m_config; | ||
| 436 | |||
| 437 | std::shared_ptr<WinFileStream> m_file_stream; | ||
| 438 | std::shared_ptr<std::mutex> m_log_mutex; | ||
| 439 | |||
| 440 | std::jthread m_writer_thread; | ||
| 441 | std::atomic<bool> m_running{false}; | ||
| 442 | std::atomic<bool> m_shutdown_requested{false}; | ||
| 443 | |||
| 444 | std::mutex m_flush_mutex; | ||
| 445 | std::condition_variable m_flush_cv; | ||
| 446 | |||
| 447 | // Set true by the writer immediately before it parks on m_flush_cv and cleared when it wakes. | ||
| 448 | // Producers read it outside m_flush_mutex after a successful queue push; the seq_cst order shared | ||
| 449 | // with m_pending_messages makes a racing push either visible to the writer's wait predicate or | ||
| 450 | // visible here as a parked-writer wake. | ||
| 451 | std::atomic<bool> m_writer_waiting{false}; | ||
| 452 | |||
| 453 | std::atomic<size_t> m_pending_messages{0}; | ||
| 454 | std::atomic<size_t> m_dropped_messages{0}; | ||
| 455 | }; | ||
| 456 | |||
| 457 | } // namespace DetourModKit | ||
| 458 | |||
| 459 | #endif // DETOURMODKIT_ASYNC_LOGGER_HPP | ||
| 460 |