Skip to content

Commit dea5caf

Browse files
committed
feat(logging): add dedicated executor controls
Expose config-first, explicit, and dedicated registration macros for built-in async loggers while preserving the existing macro API. Add dedicated executor lifecycle handling, cooperative single-threaded Emscripten queueing, wakeup fixes for blocked producers, and coverage for shutdown, macro registration, and C++11 Windows debug construction.
1 parent 467b0ec commit dea5caf

26 files changed

Lines changed: 2538 additions & 49 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,12 @@ jobs:
276276
-DLOGIT_CPP_BUILD_TESTS=ON \
277277
-DLOGIT_EMSCRIPTEN=ON -DLOGIT_FORCE_ASYNC_OFF=ON
278278
- name: Build
279-
run: cmake --build build-ems --target ems_console ems_async_flush -j
279+
run: cmake --build build-ems --target ems_console ems_async_flush ems_single_thread_executor -j
280280
- name: Run smoke tests
281281
run: |
282282
node --no-experimental-fetch build-ems/tests/ems_console.js
283283
node --no-experimental-fetch build-ems/tests/ems_async_flush.js
284+
node --no-experimental-fetch build-ems/tests/ems_single_thread_executor.js
284285
- name: Upload logs
285286
if: failure()
286287
uses: actions/upload-artifact@v4

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,40 @@ your workload needs a different baseline. See
212212
[`docs/TaskExecutor.md`](docs/TaskExecutor.md) for a full breakdown and tuning
213213
tips.
214214

215+
Logger backend `Config` structs can opt into `use_dedicated_executor=true` when
216+
one slow sink must not delay other async loggers. On native builds this creates
217+
one worker thread per configured logger, so use it deliberately for expensive or
218+
isolated backends. Single-threaded Emscripten builds keep the same per-instance
219+
queue semantics but drain cooperatively on the browser event loop.
220+
221+
You can always pass a configured backend through the generic macro:
222+
223+
```cpp
224+
logit::ConsoleLogger::Config cfg;
225+
cfg.async = true;
226+
cfg.use_dedicated_executor = true;
227+
cfg.queue_capacity = 1024;
228+
cfg.queue_policy = logit::detail::QueuePolicy::Block;
229+
230+
LOGIT_ADD_LOGGER(
231+
logit::ConsoleLogger,
232+
(cfg),
233+
logit::SimpleLogFormatter,
234+
(LOGIT_CONSOLE_PATTERN)
235+
);
236+
```
237+
238+
Built-in helpers also expose config-first and short dedicated forms:
239+
240+
```cpp
241+
LOGIT_ADD_CONSOLE_CONFIG(cfg, LOGIT_CONSOLE_PATTERN);
242+
LOGIT_ADD_CONSOLE_DEDICATED(
243+
LOGIT_CONSOLE_PATTERN,
244+
1024,
245+
logit::detail::QueuePolicy::DropNewest
246+
);
247+
```
248+
215249
## Features
216250

217251
- **Flexible Log Formatting**:

docs/TaskExecutor.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ outlive static destructors inside logger components. Applications may call
108108
`shutdown()` explicitly (for example during test teardown), but the singleton
109109
remains valid until the process terminates.
110110

111+
Logger backends with `Config::use_dedicated_executor=true` own a
112+
`SingleThreadExecutor` instead of using this singleton. Native builds create one
113+
worker thread per configured logger, while single-threaded Emscripten builds use
114+
a cooperative per-instance queue. `Logger::shutdown()` calls each backend's
115+
`ILogger::shutdown()` hook before stopping the global executor so these
116+
logger-owned workers drain and stop cleanly.
117+
111118
## 6. Emscripten (no pthreads)
112119

113120
When targeting Emscripten without pthread support:
@@ -118,6 +125,8 @@ When targeting Emscripten without pthread support:
118125
* Tasks are executed by `emscripten_async_call`, which schedules a drain on the
119126
browser event loop. This keeps logging compatible with the cooperative
120127
execution model used in WebAssembly UI scenarios.
128+
* Dedicated logger executors use the same cooperative scheduling model in this
129+
build; no OS thread is created.
121130
* Typical use cases: browser-hosted tools or demos that need asynchronous-style
122131
logging without pulling in pthread support.
123132

docs/mainpage.dox

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,33 @@ LOGIT_SET_QUEUE_POLICY(LOGIT_QUEUE_BLOCK); // Block when full
225225
\endcode
226226
Available policies: `LOGIT_QUEUE_DROP_NEWEST`, `LOGIT_QUEUE_DROP_OLDEST`, `LOGIT_QUEUE_BLOCK`.
227227

228+
Backend `Config` structs can set `use_dedicated_executor=true` to isolate a slow
229+
async sink from the global task executor. Native builds create one worker thread
230+
per configured logger; single-threaded Emscripten builds use a cooperative
231+
per-instance queue instead.
232+
233+
\code{.cpp}
234+
logit::ConsoleLogger::Config cfg;
235+
cfg.async = true;
236+
cfg.use_dedicated_executor = true;
237+
cfg.queue_capacity = 1024;
238+
cfg.queue_policy = logit::detail::QueuePolicy::Block;
239+
240+
LOGIT_ADD_LOGGER(
241+
logit::ConsoleLogger,
242+
(cfg),
243+
logit::SimpleLogFormatter,
244+
(LOGIT_CONSOLE_PATTERN)
245+
);
246+
247+
LOGIT_ADD_CONSOLE_CONFIG(cfg, LOGIT_CONSOLE_PATTERN);
248+
LOGIT_ADD_CONSOLE_DEDICATED(
249+
LOGIT_CONSOLE_PATTERN,
250+
1024,
251+
logit::detail::QueuePolicy::DropNewest
252+
);
253+
\endcode
254+
228255
\subsection stream_logging Stream-Based Logging
229256

230257
Use stream operators for complex messages.

include/logit_cpp/logit/Logger.hpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,8 @@ namespace logit {
388388
///
389389
/// Ensures that all log messages are fully processed before continuing.
390390
void wait() {
391-
LoggerReadLock lock(m_loggers_mx);
392-
for (const auto& strategy : m_loggers) {
391+
const auto snapshot = get_all_strategy_snapshots();
392+
for (const auto& strategy : snapshot) {
393393
if (!strategy) continue;
394394
strategy->logger->wait();
395395
}
@@ -400,9 +400,14 @@ namespace logit {
400400
/// Disables further logging, waits for asynchronous tasks to complete,
401401
/// and shuts down TaskExecutor.
402402
void shutdown() {
403-
if (m_shutdown) return;
404-
m_shutdown = true;
405-
wait();
403+
if (m_shutdown.exchange(true)) return;
404+
405+
const auto snapshot = get_all_strategy_snapshots();
406+
for (const auto& strategy : snapshot) {
407+
if (!strategy) continue;
408+
std::lock_guard<std::mutex> exec_lock(strategy->exec_mx);
409+
strategy->logger->shutdown();
410+
}
406411
detail::TaskExecutor::get_instance().shutdown();
407412
}
408413

@@ -441,6 +446,11 @@ namespace logit {
441446
return std::shared_ptr<LoggerStrategy>();
442447
}
443448

449+
std::vector<std::shared_ptr<LoggerStrategy>> get_all_strategy_snapshots() const {
450+
LoggerReadLock lock(m_loggers_mx);
451+
return m_loggers;
452+
}
453+
444454
std::vector<std::shared_ptr<LoggerStrategy>> m_loggers; ///< Container for logger-formatter pairs.
445455
mutable LoggerMutex m_loggers_mx; ///< Protects access to logger strategies.
446456
std::atomic<bool> m_shutdown = ATOMIC_VAR_INIT(false); ///< Flag indicating if shutdown was requested.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
#ifndef _LOGIT_DETAIL_QUEUE_POLICY_HPP_INCLUDED
3+
#define _LOGIT_DETAIL_QUEUE_POLICY_HPP_INCLUDED
4+
5+
namespace logit { namespace detail {
6+
7+
/// \brief Queue overflow handling policy used by TaskExecutor and SingleThreadExecutor.
8+
enum class QueuePolicy {
9+
DropNewest, ///< Reject the incoming task when the queue is full.
10+
DropOldest, ///< Drop the oldest queued task.
11+
Block ///< Producers wait until capacity is available.
12+
};
13+
14+
}} // namespace logit::detail
15+
16+
#endif // _LOGIT_DETAIL_QUEUE_POLICY_HPP_INCLUDED

0 commit comments

Comments
 (0)