From 13f13fe038c970a46ca71845820064f3201f31a9 Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Tue, 17 Dec 2024 13:52:33 +0900 Subject: [PATCH 1/4] Add Concurrency option and MPSC support using farbot. context: #6 #14 --- CMakeLists.txt | 21 +++++++++++ README.md | 3 +- examples/everlog/everlogmain.cpp | 2 +- include/rtlog/rtlog.h | 60 ++++++++++++++++++++++++++++---- test/test_rtlog.cpp | 20 +++++++++-- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e671bb0..fc2b878 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,26 @@ if(NOT TARGET readerwriterqueue) FetchContent_MakeAvailable(ReaderWriterQueue) endif() +if (NOT TARGET farbot) + include(FetchContent) + + FetchContent_Declare(farbot + GIT_REPOSITORY https://github.com/hogliux/farbot + GIT_TAG 0416705394720c12f0d02e55c144e4f69bb06912 + ) + # Note we do not "MakeAvailable" here, because farbot does not fully work via FetchContent + if(NOT farbot_POPULATED) + FetchContent_Populate(farbot) + endif() + add_library(farbot INTERFACE) + add_library(farbot::farbot ALIAS farbot) + + target_include_directories(farbot INTERFACE + $ + $ + ) +endif() + if(NOT TARGET stb::stb) # Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") @@ -73,6 +93,7 @@ target_link_libraries(rtlog INTERFACE readerwriterqueue stb::stb + farbot::farbot $<$:fmt::fmt> ) diff --git a/README.md b/README.md index 335322a..6693987 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,14 @@ The design behind this logger was presented at ADCx 2023. Presentation [video](h - Ability to log messages of any type and size from the real-time thread - Statically allocated memory at compile time, no allocations in the real-time thread - Support for printf-style format specifiers (using [a version of the printf family](https://github.com/nothings/stb/blob/master/stb_sprintf.h) that doesn't hit the `localeconv` lock) -- Efficient thread-safe logging using a [lock free queue](https://github.com/cameron314/readerwriterqueue). +- Efficient thread-safe logging using lock free queues (either [single consumer](https://github.com/cameron314/readerwriterqueue) or [multi consumer](https://github.com/hogliux/farbot)). ## Requirements - A C++17 compatible compiler - The C++17 standard library - moodycamel::ReaderWriterQueue (will be downloaded via cmake if not provided) +- farbot's fifo (will be downloaded via cmake if not provided) - stb's vsnprintf (will be downloaded via cmake if not provided) ## Installation via CMake diff --git a/examples/everlog/everlogmain.cpp b/examples/everlog/everlogmain.cpp index a76d93d..9ddcd04 100644 --- a/examples/everlog/everlogmain.cpp +++ b/examples/everlog/everlogmain.cpp @@ -4,7 +4,7 @@ namespace evr { constexpr auto MAX_LOG_MESSAGE_LENGTH = 256; -constexpr auto MAX_NUM_LOG_MESSAGES = 100; +constexpr auto MAX_NUM_LOG_MESSAGES = 128; enum class LogLevel { Debug, Info, Warning, Critical }; diff --git a/include/rtlog/rtlog.h b/include/rtlog/rtlog.h index fc6a895..7de0c84 100644 --- a/include/rtlog/rtlog.h +++ b/include/rtlog/rtlog.h @@ -10,6 +10,7 @@ #include #endif // RTLOG_USE_FMTLIB +#include #include #ifndef STB_SPRINTF_IMPLEMENTATION @@ -31,6 +32,11 @@ enum class Status { Error_MessageTruncated = 2, }; +enum class QueueConcurrency { + Single_Producer_Single_Consumer = 0, + Multi_Producer_Single_Consumer = 1, +}; + /** * @brief A logger class for logging messages. * This class allows you to log messages of type LogData. @@ -38,7 +44,8 @@ enum class Status { * format string you want to log. For instance: The log level, the log region, * the file name, the line number, etc. See examples or tests for some ideas. * - * TODO: Currently is built on a single input/single output queue. Do not call + * NOTE: by default, it is built on a single input/single output queue. You have + * to specify QueueConcurrency for other types of queues. Otherwise, do not call * Log or PrintAndClearLogQueue from multiple threads. * * @tparam LogData The type of the data to be logged. @@ -49,9 +56,16 @@ enum class Status { * @tparam SequenceNumber This number is incremented when the message is * enqueued. It is assumed that your non-realtime logger increments and logs it * on Log. + * @tparam QueueConcurrency The concurrency type of the internal queue. + * The default Single_Producer_Single_Consumer is for the simplest queue that + * works in single-producer thread model. + * Multi_Producer_Single_Consumer is for such an application that needs to + * handle multiple logging clients. */ template &SequenceNumber> + std::atomic &SequenceNumber, + QueueConcurrency Concurrency = + QueueConcurrency::Single_Producer_Single_Consumer> class Logger { public: /* @@ -97,7 +111,7 @@ class Logger { // Even if the message was truncated, we still try to enqueue it to minimize // data loss - const bool dataWasEnqueued = mQueue.try_enqueue(dataToQueue); + const bool dataWasEnqueued = mQueue->tryEnqueue(std::move(dataToQueue)); if (!dataWasEnqueued) retVal = Status::Error_QueueFull; @@ -196,7 +210,7 @@ class Logger { // Even if the message was truncated, we still try to enqueue it to minimize // data loss - const bool dataWasEnqueued = mQueue.try_enqueue(dataToQueue); + const bool dataWasEnqueued = mQueue->tryEnqueue(std::move(dataToQueue)); if (!dataWasEnqueued) retVal = Status::Error_QueueFull; @@ -227,7 +241,7 @@ class Logger { int numProcessed = 0; InternalLogData value; - while (mQueue.try_dequeue(value)) { + while (mQueue->tryDequeue(value)) { printLogFn(value.mLogData, value.mSequenceNumber, "%s", value.mMessage.data()); numProcessed++; @@ -243,7 +257,41 @@ class Logger { std::array mMessage{}; }; - moodycamel::ReaderWriterQueue mQueue{MaxNumMessages}; + class InternalQueue { + public: + virtual bool tryEnqueue(InternalLogData &&value) = 0; + virtual bool tryDequeue(InternalLogData &value) = 0; + }; + class InternalQueueSPSC : public InternalQueue { + moodycamel::ReaderWriterQueue mQueue{MaxNumMessages}; + + public: + bool tryEnqueue(InternalLogData &&value) override { + return mQueue.try_enqueue(std::move(value)); + } + bool tryDequeue(InternalLogData &value) override { + return mQueue.try_dequeue(value); + } + }; + class InternalQueueMPSC : public InternalQueue { + farbot::fifo + mQueue{MaxNumMessages}; + + public: + bool tryEnqueue(InternalLogData &&value) override { + return mQueue.push(std::move(value)); + } + bool tryDequeue(InternalLogData &value) override { + return mQueue.pop(value); + } + }; + + std::unique_ptr mQueue{ + Concurrency == QueueConcurrency::Single_Producer_Single_Consumer + ? (std::unique_ptr) + std::make_unique() + : std::make_unique()}; }; /** diff --git a/test/test_rtlog.cpp b/test/test_rtlog.cpp index 415b599..a712da6 100644 --- a/test/test_rtlog.cpp +++ b/test/test_rtlog.cpp @@ -6,7 +6,7 @@ namespace rtlog::test { std::atomic gSequenceNumber{0}; constexpr auto MAX_LOG_MESSAGE_LENGTH = 256; -constexpr auto MAX_NUM_LOG_MESSAGES = 100; +constexpr auto MAX_NUM_LOG_MESSAGES = 128; enum class ExampleLogLevel { Debug, Info, Warning, Critical }; @@ -81,6 +81,22 @@ TEST_CASE("Test rtlog basic construction") { CHECK(logger.PrintAndClearLogQueue(PrintMessage) == 4); } +TEST_CASE("Test rtlog MPSC basic construction") { + rtlog::Logger + logger; + logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, + "Hello, world!"); + logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, "Hello, world!"); + logger.Log({ExampleLogLevel::Warning, ExampleLogRegion::Network}, + "Hello, world!"); + logger.Log({ExampleLogLevel::Critical, ExampleLogRegion::Audio}, + "Hello, world!"); + + CHECK(logger.PrintAndClearLogQueue(PrintMessage) == 4); +} + TEST_CASE("va_args works as intended") { rtlog::Logger @@ -199,7 +215,7 @@ TEST_CASE("Errors are returned from Log") { } SUBCASE("Enqueue more than capacity and get an error") { - const auto maxNumMessages = 10; + const auto maxNumMessages = 16; rtlog::Logger logger; From e6ee502c9308f060f683bf835c745a65c92e7fda Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Fri, 20 Dec 2024 23:47:00 +0900 Subject: [PATCH 2/4] InternalQueue was missing virtual destructor. --- include/rtlog/rtlog.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/rtlog/rtlog.h b/include/rtlog/rtlog.h index 7de0c84..54a9d53 100644 --- a/include/rtlog/rtlog.h +++ b/include/rtlog/rtlog.h @@ -259,6 +259,7 @@ class Logger { class InternalQueue { public: + virtual ~InternalQueue() = default; virtual bool tryEnqueue(InternalLogData &&value) = 0; virtual bool tryDequeue(InternalLogData &value) = 0; }; From 91730321910101b6320713517559337b6000279a Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Sat, 21 Dec 2024 17:57:42 +0900 Subject: [PATCH 3/4] Add static assertion on MaxNumMessages when farbot MPSC queue is used. --- include/rtlog/rtlog.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/rtlog/rtlog.h b/include/rtlog/rtlog.h index 54a9d53..0c015aa 100644 --- a/include/rtlog/rtlog.h +++ b/include/rtlog/rtlog.h @@ -280,6 +280,11 @@ class Logger { mQueue{MaxNumMessages}; public: + InternalQueueMPSC() { + static_assert((MaxNumMessages & (MaxNumMessages - 1)) == 0 || + Concurrency != QueueConcurrency::Multi_Producer_Single_Consumer, + "you have to assign 2^n to MaxNumMessages (farbot backend restriction)"); + } bool tryEnqueue(InternalLogData &&value) override { return mQueue.push(std::move(value)); } From 46e6306dde68c102a733654b98494b5bc8825874 Mon Sep 17 00:00:00 2001 From: Atsushi Eno Date: Sat, 21 Dec 2024 17:58:25 +0900 Subject: [PATCH 4/4] Specify explicit failure modes on farbot fifo. --- include/rtlog/rtlog.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/rtlog/rtlog.h b/include/rtlog/rtlog.h index 0c015aa..5d29821 100644 --- a/include/rtlog/rtlog.h +++ b/include/rtlog/rtlog.h @@ -276,7 +276,9 @@ class Logger { }; class InternalQueueMPSC : public InternalQueue { farbot::fifo + farbot::fifo_options::concurrency::multiple, + farbot::fifo_options::full_empty_failure_mode::return_false_on_full_or_empty, + farbot::fifo_options::full_empty_failure_mode::overwrite_or_return_default> mQueue{MaxNumMessages}; public: