diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..3c52749 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,10 @@ +Checks: '-*,modernize-*,cppcoreguidelines-*,-modernize-use-trailing-return-type' +WarningsAsErrors: 'modernize-use-nullptr' +HeaderFilterRegex: '.*(my_project/include|my_project/src)/.*' +CheckOptions: + - key: modernize-use-nullptr.NullMacros + value: NULL,CUSTOM_NULL + - key: cppcoreguidelines-pro-type-member-init.CheckNakedFields + value: true + - key: readability-braces-around-statements.ShortStatementLines + value: 1 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 44d118d..bde184e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,9 @@ add_library(${PROJECT_NAME} SHARED) target_include_directories(${PROJECT_NAME} PUBLIC include) +set(BENCH ON) +set(TESTS ON) + file(GLOB prj_src src/*) target_sources(${PROJECT_NAME} PRIVATE ${prj_src}) @@ -14,5 +17,14 @@ target_sources(${PROJECT_NAME} PRIVATE ${prj_src}) # target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=thread) # target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=thread) -add_subdirectory(bench) -add_subdirectory(tests) \ No newline at end of file +if(DEFINED BENCH) + add_subdirectory(bench) +endif() + +if(DEFINED TESTS) + add_subdirectory(tests) +endif() + +install(TARGETS ${PROJECT_NAME} DESTINATION lib) +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/" + DESTINATION include) \ No newline at end of file diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json index 66dabec..945b382 100644 --- a/CMakeUserPresets.json +++ b/CMakeUserPresets.json @@ -4,7 +4,6 @@ "conan": {} }, "include": [ - "build/Release/generators/CMakePresets.json", - "build/Debug/generators/CMakePresets.json" + "build/CMakePresets.json" ] } \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7dd22b --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# MetricsLogger + +Simple implementation of metrics logger with next constraints: +1. no blocking on worker threads +2. metrics only of arithmetic types +3. one metrics is written only by one thread + +```sh +Benchmark Time CPU Iterations +--------------------------------------------------------------- +BM_wo_logger 662 ns 662 ns 1061318 +BM_taylor_logger 886 ns 886 ns 796535 +BM_taylor_glog 4303 ns 4302 ns 162679 +BM_taylor_atomic_upd 671 ns 671 ns 1049589 +BM_taylor_mutex_upd 725 ns 725 ns 960519 +BM_taylor_map_upd 944 ns 943 ns 744620 +BM_caesar_logger 52965 ns 52963 ns 13206 +BM_caesar_wo_logger 52636 ns 52635 ns 13284 +DoNothing 4.74 ns 4.74 ns 112688754 +``` \ No newline at end of file diff --git a/bench/main.cc b/bench/main.cc index 3623c28..66f8cd1 100644 --- a/bench/main.cc +++ b/bench/main.cc @@ -1,11 +1,10 @@ #include #include +#include #include -#include #include #include "benchmark/benchmark.h" #include "glog/logging.h" -#include "logger.hh" //! AI generated static double taylor_approximation( @@ -38,16 +37,16 @@ static double calc_exp_taylor() { auto exp_deriv = [](int k, double a) -> double { return std::exp(a); // k-th derivative of e^x is e^x }; - double x = 1.0; - double a_exp = 0.0; - int terms = 10; + static constexpr double x = 1.0; + static constexpr double a_exp = 0.0; + static constexpr int terms = 10; return taylor_approximation( x, a_exp, terms, [](double a) { return std::exp(a); }, // f(a) exp_deriv); } -vptyp::Logger& getLogger() { - static vptyp::Logger l(std::cout); +vptyp::MetricsLogger& getLogger() { + static vptyp::MetricsLogger l(std::cout); if (!l.isConfigured()) { l.configure({"size", "apprx", "garbage", "garbage2"}); } @@ -63,9 +62,10 @@ void initGlog() { static void BM_taylor_logger(benchmark::State& state) { auto& l = getLogger(); + std::string apprx = "apprx"; for (auto _ : state) { double res = calc_exp_taylor(); - l.add("apprx", res); + l.add(apprx, res); } } @@ -95,18 +95,16 @@ static void BM_taylor_atomic_upd(benchmark::State& state) { } void updMapValue(double res, const std::string& key) { - static std::unordered_map d = {{"key", 10.0}, - {"assembly", 11.0}, - {"draw", 123.3}, - {"d2", 0.0}, - {"d55", 0.23}}; + static std::unordered_map d = { + {"key", 0}, {"assembly", 0}, {"draw", 0.0}, {"d2", 0.0}, {"d55", 0.0}}; d[key] = res; } static void BM_taylor_map_upd(benchmark::State& state) { + std::string assembly = "assembly"; for (auto _ : state) { double res = calc_exp_taylor(); - updMapValue(res, "assembly"); + updMapValue(res, assembly); } } @@ -136,12 +134,13 @@ BENCHMARK(BM_taylor_map_upd); std::string caesar_encoder(const std::string& input) { static constexpr int small = 'a'; static constexpr int big = 'A'; + static constexpr int alphabetSize = 25; std::string encoded; encoded.reserve(input.size()); for (auto& c : input) { - assert((c - small < 25 && c - small >= 0) || - (c - big < 25 && c - big >= 0)); - int id, begin; + assert((c - small < alphabetSize && c - small >= 0) || + (c - big < alphabetSize && c - big >= 0)); + int id{0}, begin{0}; if (c - small < 0) { id = c - big; begin = big; @@ -149,12 +148,13 @@ std::string caesar_encoder(const std::string& input) { id = c - small; begin = small; } - encoded += begin + (id + 3) % 25; + encoded += begin + (id + 3) % alphabetSize; } return encoded; } -int caesar_base(int size = 1000) { +static constexpr int sizeDef = 1000; +int caesar_base(int size = sizeDef) { std::string a(size, 'a'); auto res = caesar_encoder(a); return size; diff --git a/conanfile.txt b/conanfile.txt index 883700e..6eb3f11 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,6 +1,7 @@ [requires] gtest/1.16.0 -glog/0.7.1 +rapidyaml/0.9.0 +glog/0.7.1-unwindfix benchmark/1.9.1 boost/1.84.0 @@ -13,6 +14,4 @@ boost/*:without_* = True [generators] CMakeDeps -CMakeToolchain -[layout] -cmake_layout \ No newline at end of file +CMakeToolchain \ No newline at end of file diff --git a/include/logger.hh b/include/metricsLogger.hh similarity index 62% rename from include/logger.hh rename to include/metricsLogger.hh index cb38195..c7ee357 100644 --- a/include/logger.hh +++ b/include/metricsLogger.hh @@ -13,29 +13,28 @@ namespace vptyp { static constexpr std::string_view configErrorMsg = "Bruh, incorrect configuration"; -class Logger { - // helper class for handling worker thread - class Worker; - +class MetricsLogger { using map_type = std::unordered_map>; public: - virtual ~Logger(); - - Logger(); - explicit Logger(std::ostream& out); - Logger(Logger&) = delete; - Logger(Logger&&) = delete; - Logger& operator=(Logger&) = delete; - Logger& operator=(Logger&&) = delete; + virtual ~MetricsLogger(); + MetricsLogger(); + explicit MetricsLogger(std::ostream& out); + MetricsLogger(MetricsLogger&) = delete; + MetricsLogger(MetricsLogger&&) = delete; + MetricsLogger& operator=(MetricsLogger&) = delete; + MetricsLogger& operator=(MetricsLogger&&) = delete; + /// @brief you should make logger configuration before logging + /// After logger will be ready for holding metrics. /// @return success or not (already configured) bool configure(const std::vector& d); - template >> - void add(const std::string& field, Metric metric) { + template + void add(const std::string& field, Metric metric) + requires(std::is_arithmetic_v) + { refs.fetch_add(1, std::memory_order_release); map_type* locked = active.load(std::memory_order_acquire); auto it = locked->find(field); @@ -50,14 +49,16 @@ class Logger { bool isConfigured() { return configured == CONFIGURED; } private: + // helper class for handling worker thread + class Worker; friend Worker; enum Configuration { NOT_CONFIGURED, CONFIG_IN_PROGRESS, CONFIGURED }; std::atomic configured{NOT_CONFIGURED}; std::unique_ptr worker; std::unique_ptr m1, m2; - std::atomic active; // impl may use mutex! - std::atomic refs{0}; // degradation on worker side (waiting for no - // one to be in refs section) + std::atomic active; + std::atomic refs{0}; // degradation on worker side (waiting for no + // one to be in refs section) }; } // namespace vptyp \ No newline at end of file diff --git a/src/logger.cc b/src/logger.cc deleted file mode 100644 index c606400..0000000 --- a/src/logger.cc +++ /dev/null @@ -1,102 +0,0 @@ -#include "logger.hh" -#include -#include -#include -namespace vptyp { - -bool Logger::configure(const std::vector& d) { - int tmp = configured.load(); - if (tmp || !configured.compare_exchange_weak(tmp, CONFIG_IN_PROGRESS)) - return false; - - auto& m1_ref = *m1.get(); - auto& m2_ref = *m2.get(); - for (auto& key : d) { - m1_ref[key] = 0; - } - m2_ref = m1_ref; - active.store(m1.get()); - configured.store(CONFIGURED); - return true; -} - -Logger::Logger() - : m1(std::make_unique()), m2(std::make_unique()) { - worker = std::make_unique(*this, std::cout); -} - -Logger::Logger(std::ostream& out) - : m1(std::make_unique()), m2(std::make_unique()) { - worker = std::make_unique(*this, out); -} - -Logger::~Logger() { - worker.reset(); -} - -class Logger::Worker { - public: - explicit Worker(Logger& father, std::ostream& out) - : parent(father), out(out) { - thread = std::thread([this] { routine(); }); - } - - ~Worker() { - state = STOPPING; - thread.join(); - } - - void routine() { - state = RUNNING; - while (state == RUNNING) { - unroll(); - sleep(1); - } - } - - void unroll(); - - private: - enum State { UNDEF, RUNNING, STOPPING }; - std::atomic state; - Logger& parent; - std::ostream& out; - std::thread thread; // jthread not needed, as we anyway must wait for join -}; - -void Logger::Worker::unroll() { - if (!parent.isConfigured()) - return; - - auto tmp = parent.active.load(std::memory_order_acquire); - auto toBeActive = tmp == parent.m1.get() ? parent.m2.get() : parent.m1.get(); - - parent.active.store(toBeActive, std::memory_order_release); - - // so we setting up happens before relation with counters - while (parent.refs.load(std::memory_order_acquire) > 0) { - std::this_thread::yield(); - } - - // // it's needed thread fence to guarantee use count change - // // __tsan_acquire(tmp.use_count()); - // std::atomic_thread_fence(std::memory_order_acquire); - - // at this place we are guarantee that tmp is only ours or not? - std::string output; - bool haveToPush{false}; - for (auto& element : *tmp) { - if (!std::visit([](auto&& i) -> bool { return i; }, element.second)) - continue; - std::string value = - std::visit([](auto&& i) { return std::to_string(i); }, element.second); - output += "[" + element.first + "=" + value + "] "; - element.second = 0l; - haveToPush = true; - } - - if (haveToPush) - out << output << std::endl; -} - -} // namespace vptyp \ No newline at end of file diff --git a/src/metricsLogger.cc b/src/metricsLogger.cc new file mode 100644 index 0000000..12c00cf --- /dev/null +++ b/src/metricsLogger.cc @@ -0,0 +1,147 @@ +#include +#include +#include +#include +#include +#include +#include +namespace vptyp { + +/** + * @brief take yaml file and parse it + * @details + */ +class LoggerFormat { + public: + explicit LoggerFormat(std::string_view format_file = "logger-format.yaml") { + (void)format_file; // unused at the moment + } + + void placeElement(std::string_view key, std::string_view value); + void finalize(std::ostream& out); + + private: + pid_t pid = getpid(); + std::string time_format{"%d-%m-%Y %H-%M-%S"}; + std::string prefix{"[{}] {} "}; + std::string elementFormat{"{}={}"}; + std::string delimiter{";"}; + std::string intermediateLine{}; +}; + +bool MetricsLogger::configure(const std::vector& d) { + int tmp = configured.load(); + if (tmp || !configured.compare_exchange_weak(tmp, CONFIG_IN_PROGRESS)) + return false; + + auto& m1_ref = *m1.get(); + auto& m2_ref = *m2.get(); + for (auto& key : d) { + m1_ref[key] = 0; + } + m2_ref = m1_ref; + active.store(m1.get()); + configured.store(CONFIGURED); + return true; +} + +MetricsLogger::MetricsLogger() + : m1(std::make_unique()), m2(std::make_unique()) { + worker = std::make_unique(*this, std::cout); +} + +MetricsLogger::MetricsLogger(std::ostream& out) + : m1(std::make_unique()), m2(std::make_unique()) { + worker = std::make_unique(*this, out); +} + +MetricsLogger::~MetricsLogger() { + worker.reset(); +} + +class MetricsLogger::Worker { + public: + explicit Worker(MetricsLogger& father, std::ostream& out) + : parent(father), out(out) { + thread = std::thread([this] { routine(); }); + } + + ~Worker() { + state = STOPPING; + thread.join(); + } + + Worker(const Worker&) = delete; + Worker(Worker&&) = delete; + Worker& operator=(const Worker&) = delete; + Worker& operator=(const Worker&&) = delete; + + void routine() { + state = RUNNING; + while (state == RUNNING) { + unroll(); + sleep(1); + } + } + + void unroll(); + + private: + LoggerFormat formatter; + enum State { UNDEF, RUNNING, STOPPING }; + std::atomic state; + MetricsLogger& parent; + std::ostream& out; + std::thread thread; // jthread not needed, as we anyway must wait for join +}; + +void MetricsLogger::Worker::unroll() { + if (!parent.isConfigured()) + return; + + auto tmp = parent.active.load(std::memory_order_acquire); + auto toBeActive = tmp == parent.m1.get() ? parent.m2.get() : parent.m1.get(); + + parent.active.store(toBeActive, std::memory_order_release); + + // so we setting up happens before relation with counters + while (parent.refs.load(std::memory_order_acquire) > 0) { + std::this_thread::yield(); + } + + // at this place we are guarantee that tmp is only ours :) + std::string output; + bool haveToPush{false}; + for (auto& element : *tmp) { + if (!std::visit([](auto&& i) -> bool { return i; }, element.second)) + continue; + std::string value = + std::visit([](auto&& i) { return std::to_string(i); }, element.second); + formatter.placeElement(element.first, value); + element.second = 0l; + haveToPush = true; + } + + if (haveToPush) + formatter.finalize(out); +} + +inline void LoggerFormat::placeElement(std::string_view key, + std::string_view value) { + if (!intermediateLine.empty()) + intermediateLine += delimiter; + intermediateLine = + std::vformat(elementFormat, std::make_format_args(key, value)); +} + +inline void LoggerFormat::finalize(std::ostream& out) { + auto t = std::time(nullptr); + auto tm = *std::localtime(&t); + std::stringstream ss; + ss << std::put_time(&tm, this->time_format.c_str()); + auto timeString = ss.str(); + out << std::vformat(prefix, std::make_format_args(timeString, pid)) << " " + << intermediateLine << std::endl; +} + +} // namespace vptyp \ No newline at end of file diff --git a/tests/main.cc b/tests/main.cc index af3adbb..e67ecf6 100644 --- a/tests/main.cc +++ b/tests/main.cc @@ -1,11 +1,11 @@ #include #include -#include +#include #include #include TEST(SingleThread, Configuration) { - vptyp::Logger l(std::cout); + vptyp::MetricsLogger l(std::cout); EXPECT_NO_THROW(l.configure({"apprx", "size", "time"})); EXPECT_NO_THROW(l.add("apprx", 123)); EXPECT_ANY_THROW(l.add("not in configuration", 123)); @@ -15,7 +15,7 @@ void outBufferCheck(std::ostringstream& s, std::queue>& prev) { std::string_view outBufferView = s.rdbuf()->view(); std::string_view lineView; - auto startPos = 0; + size_t startPos = 0; while (startPos < outBufferView.length() && (lineView = outBufferView.substr(startPos, outBufferView.find('\n')), lineView.size() > 0)) { @@ -44,19 +44,19 @@ TEST(SingleThread, Add) { std::ostringstream s; std::queue> prev; { - vptyp::Logger l(s); + vptyp::MetricsLogger l(s); l.configure({"apprx", "size", "time"}); auto decorator = [&l, &prev](std::string field, int value) { l.add(field, value); - prev.push({field, value}); + prev.emplace(field, value); }; decorator("size", 1); decorator("time", 1); sleep(2); // twice of logger sleep - decorator("apprx", 12); + decorator("apprx", 1); decorator("size", 2); sleep(2); } @@ -66,7 +66,8 @@ TEST(SingleThread, Add) { } TEST(MultiThread, Configure) { - vptyp::Logger l(std::cout); + static constexpr size_t threadsNumber = 10; + vptyp::MetricsLogger l(std::cout); std::atomic howManyConfigured; auto fConfig = [&l, &howManyConfigured] { bool res{false}; @@ -74,7 +75,7 @@ TEST(MultiThread, Configure) { howManyConfigured.fetch_add(static_cast(res)); }; // simulate race condition configure - std::vector threads(10); + std::vector threads(threadsNumber); for (auto& thread : threads) { thread = std::jthread(fConfig); } @@ -83,28 +84,30 @@ TEST(MultiThread, Configure) { } TEST(MultiThread, Add) { + static constexpr size_t maxRandNumber = 100; + static constexpr size_t producerIterations = 5; std::ostringstream s; std::queue> prev; std::mutex mtx; { - vptyp::Logger l(s); + vptyp::MetricsLogger l(s); l.configure({"apprx", "size", "time"}); auto decorator = [&l, &prev, &mtx](std::string field, int value) { - for (auto i = 0; i < 5; ++i) { + for (size_t i = 0; i < producerIterations; ++i) { l.add(field, value); { std::lock_guard g(mtx); - prev.push({field, value}); + prev.emplace(field, value); } sleep(1); } }; std::vector threads(3); - threads[0] = std::jthread(decorator, "apprx", rand() % 100); - threads[1] = std::jthread(decorator, "size", rand() % 100); - threads[2] = std::jthread(decorator, "time", rand() % 100); + threads[0] = std::jthread(decorator, "apprx", rand() % maxRandNumber); + threads[1] = std::jthread(decorator, "size", rand() % maxRandNumber); + threads[2] = std::jthread(decorator, "time", rand() % maxRandNumber); threads.clear(); sleep(1);