diff --git a/CMakeLists.txt b/CMakeLists.txt index 4925ff3..a43966a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,14 +43,24 @@ target_include_directories(${PROJECT_NAME} INTERFACE $ ) -# Try to find the package ReaderWriterQueue, if not found fetch with FetchContent -find_package(ReaderWriterQueue QUIET) -if(NOT TARGET readerwriterqueue) +if (NOT TARGET farbot) include(FetchContent) - FetchContent_Declare(ReaderWriterQueue - GIT_REPOSITORY https://github.com/cameron314/readerwriterqueue + + 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 + $ + $ ) - FetchContent_MakeAvailable(ReaderWriterQueue) endif() if(NOT RTSAN_USE_FMTLIB AND NOT TARGET stb::stb) @@ -89,7 +99,7 @@ endif() target_link_libraries(rtlog INTERFACE - readerwriterqueue + farbot::farbot stb::stb $<$:fmt::fmt> ) diff --git a/README.md b/README.md index 4b5a8b4..ccc471f 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ Slides: - 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) OR support for modern libfmt formatting. -- Efficient thread-safe logging using a [lock free queue](https://github.com/cameron314/readerwriterqueue). +- Efficient thread-safe logging using a [lock free queue](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::fifo (will be downloaded via cmake if not provided) - stb's vsnprintf (will be downloaded via cmake if not provided) OR libfmt if cmake is run with the `RTSAN_USE_FMTLIB` option ## Installation via CMake @@ -130,7 +130,23 @@ Or alternatively spin up a `rtlog::LogProcessingThread` ## Customizing the queue type -If you don't want to use the SPSC moodycamel queue, you can provide your own queue type. +rtlog provides two queue type variants: `rtlog::SingleRealtimeWriterQueueType` (SPSC - default) and `rtlog::MultiRealtimeWriterQueueType` (MPSC). It is always assummed that you have one log printing thread. These may be used by specifying them: + +```cpp +using SingleWriterRtLoggerType = rtlog::Logger; + +SingleWriterRtLoggerType logger; +logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Audio}, "Hello, world! %i", 42); + +... + +using MultiWriterRtLoggerType = rtlog::Logger; + +MultiWriterRtLoggerType logger; +logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Audio}, "Hello, world! %i", 42); +``` + +If you don't want to use either of these defaults, you may provide your own queue. ** IT IS UP TO YOU TO ENSURE THE QUEUE YOU PROVIDE IS LOCK-FREE AND REAL-TIME SAFE ** diff --git a/examples/custom_queue_example/CMakeLists.txt b/examples/custom_queue_example/CMakeLists.txt index 5e3ada1..7372176 100644 --- a/examples/custom_queue_example/CMakeLists.txt +++ b/examples/custom_queue_example/CMakeLists.txt @@ -1,21 +1,10 @@ -if (NOT TARGET farbot) +find_package(ReaderWriterQueue QUIET) +if(NOT TARGET readerwriterqueue) 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 - $ - $ + FetchContent_Declare(ReaderWriterQueue + GIT_REPOSITORY https://github.com/cameron314/readerwriterqueue ) + FetchContent_MakeAvailable(ReaderWriterQueue) endif() add_executable(custom_queue_example @@ -25,5 +14,5 @@ add_executable(custom_queue_example target_link_libraries(custom_queue_example PRIVATE rtlog::rtlog - farbot::farbot + readerwriterqueue ) diff --git a/examples/custom_queue_example/customqueuemain.cpp b/examples/custom_queue_example/customqueuemain.cpp index 0bf6973..9fdad36 100644 --- a/examples/custom_queue_example/customqueuemain.cpp +++ b/examples/custom_queue_example/customqueuemain.cpp @@ -1,24 +1,20 @@ -#include #include -template class FarbotMPSCQueueWrapper { - farbot::fifo // producer_failure_mode +#include - mQueue; +template class CustomQueue { + + // technically we could use readerwriterqueue "unwrapped" but showing this off + // in the CustomQueue wrapper for documentation purposes + moodycamel::ReaderWriterQueue mQueue; public: using value_type = T; - FarbotMPSCQueueWrapper(int capacity) : mQueue(capacity) {} + CustomQueue(int capacity) : mQueue(capacity) {} - bool try_enqueue(T &&item) { return mQueue.push(std::move(item)); } - bool try_dequeue(T &item) { return mQueue.pop(item); } + bool try_enqueue(T &&item) { return mQueue.try_enqueue(std::move(item)); } + bool try_dequeue(T &item) { return mQueue.try_dequeue(item); } }; struct LogData {}; @@ -26,8 +22,7 @@ struct LogData {}; std::atomic gSequenceNumber{0}; int main() { - rtlog::Logger - logger; + rtlog::Logger logger; logger.Log({}, "Hello, World!"); logger.PrintAndClearLogQueue( diff --git a/include/rtlog/rtlog.h b/include/rtlog/rtlog.h index 351ed6f..4932f5b 100644 --- a/include/rtlog/rtlog.h +++ b/include/rtlog/rtlog.h @@ -17,7 +17,7 @@ #include #endif // RTLOG_USE_FMTLIB -#include +#include #ifdef RTLOG_USE_STB #ifndef STB_SPRINTF_IMPLEMENTATION @@ -125,12 +125,40 @@ template inline constexpr bool has_int_constructor_v = has_int_constructor::value; } // namespace detail -// On earlier versions of compilers (especially clang) you cannot -// rely on defaulted template template parameters working as intended -// This overload explicitly has 1 template paramter which is what -// `Logger` expects, it uses the default 512 from ReaderWriterQueue as -// the hardcoded MaxBlockSize -template using rtlog_SPSC = moodycamel::ReaderWriterQueue; +template +class FarbotFifoType { + farbot::fifo // producer_failure_mode + + mQueue; + +public: + using value_type = T; + + FarbotFifoType(int capacity) : mQueue(capacity) {} + + bool try_enqueue(T &&item) { return mQueue.push(std::move(item)); } + bool try_dequeue(T &item) { return mQueue.pop(item); } +}; + +template +using SingleRealtimeWriterQueueType = + FarbotFifoType; + +// NOTE: This version overwrites on full, which is a requirement to make writing +// real-time safe. +// This means it will never report Error_QueueFull. +template +using MultiRealtimeWriterQueueType = FarbotFifoType< + T, farbot::fifo_options::concurrency::multiple, + farbot::fifo_options::full_empty_failure_mode::overwrite_or_return_default>; /** * @brief A logger class for logging messages. @@ -161,12 +189,15 @@ template using rtlog_SPSC = moodycamel::ReaderWriterQueue; */ template &SequenceNumber, - template class QType = rtlog_SPSC> + template class QType = SingleRealtimeWriterQueueType> class Logger { public: using InternalLogData = detail::BasicLogData; using InternalQType = QType; + static_assert(MaxNumMessages > 0); + static_assert((MaxNumMessages & (MaxNumMessages - 1)) == 0, + "MaxNumMessages must be a power of 2"); static_assert( detail::has_int_constructor_v, "QType must have a constructor that takes an int - `QType(int)`"); diff --git a/test/test_rtlog.cpp b/test/test_rtlog.cpp index a924a1c..8760687 100644 --- a/test/test_rtlog.cpp +++ b/test/test_rtlog.cpp @@ -7,7 +7,7 @@ namespace rtlog::test { static 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 }; @@ -66,12 +66,45 @@ static auto PrintMessage = [](const ExampleLogData &data, size_t sequenceNumber, using namespace rtlog::test; +using SingleWriterRtLoggerType = + rtlog::Logger; +using MultiWriterRtLoggerType = + rtlog::Logger; + +template class RtLogTest : public ::testing::Test { +protected: + LoggerType logger_; +}; + +typedef ::testing::Types + LoggerTypes; +TYPED_TEST_SUITE(RtLogTest, LoggerTypes); + +using TruncatedSingleWriterRtLoggerType = + rtlog::Logger; +using TruncatedMultiWriterRtLoggerType = + rtlog::Logger; + +template +class TruncatedRtLogTest : public ::testing::Test { +protected: + LoggerType logger_; + inline static const size_t maxMessageLength_ = 10; +}; + +typedef ::testing::Types + TruncatedLoggerTypes; +TYPED_TEST_SUITE(TruncatedRtLogTest, TruncatedLoggerTypes); + #ifdef RTLOG_USE_STB -TEST(RtlogTest, BasicConstruction) { - rtlog::Logger - logger; +TYPED_TEST(RtLogTest, BasicConstruction) { + auto &logger = this->logger_; logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, world!"); logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, "Hello, world!"); @@ -83,11 +116,8 @@ TEST(RtlogTest, BasicConstruction) { EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 4); } -TEST(RtlogTest, VaArgsWorksAsIntended) { - rtlog::Logger - logger; - +TYPED_TEST(RtLogTest, VaArgsWorksAsIntended) { + auto &logger = this->logger_; logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, %lu!", 123ul); logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, "Hello, %f!", @@ -104,20 +134,17 @@ TEST(RtlogTest, VaArgsWorksAsIntended) { EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 6); } -void vaArgsTest(rtlog::Logger &logger, - ExampleLogData &&data, const char *format, ...) { +template +void vaArgsTest(LoggerType &&logger, ExampleLogData &&data, const char *format, + ...) { va_list args; va_start(args, format); logger.Logv(std::move(data), format, args); va_end(args); } -TEST(RtlogTest, LogvVersionWorks) { - rtlog::Logger - logger; - +TYPED_TEST(RtLogTest, LogvVersionWorks) { + auto &logger = this->logger_; vaArgsTest(logger, {ExampleLogLevel::Debug, ExampleLogRegion::Engine}, "Hello, %lu!", 123ul); vaArgsTest(logger, {ExampleLogLevel::Info, ExampleLogRegion::Game}, @@ -134,11 +161,8 @@ TEST(RtlogTest, LogvVersionWorks) { EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 6); } -TEST(RtlogTest, LoggerThreadDoesItsJob) { - rtlog::Logger - logger; - +TYPED_TEST(RtLogTest, LoggerThreadDoesItsJob) { + auto &logger = this->logger_; rtlog::LogProcessingThread thread(logger, PrintMessage, std::chrono::milliseconds(10)); @@ -158,23 +182,16 @@ TEST(RtlogTest, LoggerThreadDoesItsJob) { thread.Stop(); } -TEST(RtlogTest, ErrorsReturnedFromLog) { - rtlog::Logger - logger; - +TYPED_TEST(TruncatedRtLogTest, ErrorsReturnedFromLog) { + auto &logger = this->logger_; + auto maxMessageLength = this->maxMessageLength_; EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, - "Hello, %lu!", 123ul), + "Hello, %lu", 12ul), rtlog::Status::Success); - const auto maxMessageLength = 10; - rtlog::Logger - truncatedLogger; - EXPECT_EQ( - truncatedLogger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, - "Hello, %lu! xxxxxxxxxxx", 123ul), - rtlog::Status::Error_MessageTruncated); + EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, + "Hello, %lu! xxxxxxxxxxx", 123ul), + rtlog::Status::Error_MessageTruncated); // Inspect truncated message auto InspectLogMessage = [=](const ExampleLogData &data, @@ -193,17 +210,14 @@ TEST(RtlogTest, ErrorsReturnedFromLog) { EXPECT_STREQ(buffer.data(), "Hello, 12"); EXPECT_EQ(strlen(buffer.data()), maxMessageLength - 1); }; - EXPECT_EQ(truncatedLogger.PrintAndClearLogQueue(InspectLogMessage), 1); + EXPECT_EQ(logger.PrintAndClearLogQueue(InspectLogMessage), 2); } #endif // RTLOG_USE_STB #ifdef RTLOG_USE_FMTLIB -TEST(LoggerTest, FormatLibVersionWorksAsIntended) { - rtlog::Logger - logger; - +TYPED_TEST(RtLogTest, FormatLibVersionWorksAsIntended) { + auto &logger = this->logger_; logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, FMT_STRING("Hello, {}!"), 123l); logger.Log({ExampleLogLevel::Info, ExampleLogRegion::Game}, @@ -220,21 +234,16 @@ TEST(LoggerTest, FormatLibVersionWorksAsIntended) { EXPECT_EQ(logger.PrintAndClearLogQueue(PrintMessage), 6); } -TEST(LoggerTest, LogReturnsSuccessOnNormalEnqueue) { - rtlog::Logger - logger; +TYPED_TEST(RtLogTest, LogReturnsSuccessOnNormalEnqueue) { + auto &logger = this->logger_; EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, FMT_STRING("Hello, {}!"), 123l), rtlog::Status::Success); } -TEST(LoggerTest, LogHandlesLongMessageTruncation) { - const auto maxMessageLength = 10; - rtlog::Logger - logger; - +TYPED_TEST(TruncatedRtLogTest, LogHandlesLongMessageTruncation) { + auto &logger = this->logger_; + auto maxMessageLength = this->maxMessageLength_; EXPECT_EQ(logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, FMT_STRING("Hello, {}! xxxxxxxxxxx"), 123l), rtlog::Status::Error_MessageTruncated); @@ -260,10 +269,10 @@ TEST(LoggerTest, LogHandlesLongMessageTruncation) { EXPECT_EQ(logger.PrintAndClearLogQueue(InspectLogMessage), 1); } -TEST(LoggerTest, LogHandlesQueueFullError) { - const auto maxNumMessages = 10; +TEST(LoggerTest, SingleWriterLogHandlesQueueFullError) { + const auto maxNumMessages = 16; rtlog::Logger + gSequenceNumber, rtlog::SingleRealtimeWriterQueueType> logger; auto status = rtlog::Status::Success; @@ -276,4 +285,26 @@ TEST(LoggerTest, LogHandlesQueueFullError) { EXPECT_EQ(status, rtlog::Status::Error_QueueFull); } +TEST(LoggerTest, MultipleWriterLogHandlesNeverReturnsFull) { + const auto maxNumMessages = 16; + rtlog::Logger + logger; + + auto status = rtlog::Status::Success; + + int messageCount = 0; + + while (status == rtlog::Status::Success && + messageCount < maxNumMessages + 10) { + status = logger.Log({ExampleLogLevel::Debug, ExampleLogRegion::Engine}, + FMT_STRING("Hello, {} {}!"), "world", messageCount); + messageCount++; + } + + // We can never report full on a multi-writer queue, it is not realtime safe + // We will just happily spin forever in this loop unless we break + EXPECT_EQ(status, rtlog::Status::Success); +} + #endif // RTLOG_USE_FMTLIB