Compare commits

..

1 Commits

Author SHA1 Message Date
7199ab5f99 10ms max found on i5 12600KF 2025-07-03 21:33:30 +03:00
12 changed files with 261 additions and 295 deletions

View File

@ -1,10 +0,0 @@
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

View File

@ -7,24 +7,14 @@ add_library(${PROJECT_NAME} SHARED)
target_include_directories(${PROJECT_NAME} PUBLIC include)
set(BENCH ON)
set(TESTS ON)
file(GLOB prj_src src/*)
file(GLOB prj_src src/*.cc)
target_sources(${PROJECT_NAME} PRIVATE ${prj_src})
# target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=thread)
# target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=thread)
target_compile_definitions(${PROJECT_NAME} PRIVATE INLOOP_TIME)
if(DEFINED BENCH)
add_subdirectory(bench)
endif()
target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=thread)
target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=thread)
if(DEFINED TESTS)
add_subdirectory(tests)
endif()
install(TARGETS ${PROJECT_NAME} DESTINATION lib)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/"
DESTINATION include)
add_subdirectory(bench)
add_subdirectory(tests)

View File

@ -4,6 +4,7 @@
"conan": {}
},
"include": [
"build/CMakePresets.json"
"build/Release/generators/CMakePresets.json",
"build/Debug/generators/CMakePresets.json"
]
}

View File

@ -1,20 +0,0 @@
# 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
```

View File

@ -12,5 +12,5 @@ target_link_libraries(${NAME} PRIVATE
${PROJECT_NAME}
)
# target_compile_options(${NAME} PRIVATE -fsanitize=thread)
# target_link_options(${NAME} PRIVATE -fsanitize=thread)
target_compile_options(${NAME} PRIVATE -fsanitize=thread)
target_link_options(${NAME} PRIVATE -fsanitize=thread)

View File

@ -1,10 +1,10 @@
#include <cmath>
#include <functional>
#include <metricsLogger.hh>
#include <mutex>
#include <unordered_map>
#include "benchmark/benchmark.h"
#include "glog/logging.h"
#include "logger.hh"
//! AI generated
static double taylor_approximation(
@ -37,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
};
static constexpr double x = 1.0;
static constexpr double a_exp = 0.0;
static constexpr int terms = 10;
double x = 1.0;
double a_exp = 0.0;
int terms = 10;
return taylor_approximation(
x, a_exp, terms, [](double a) { return std::exp(a); }, // f(a)
exp_deriv);
}
vptyp::MetricsLogger& getLogger() {
static vptyp::MetricsLogger l(std::cout);
vptyp::Logger& getLogger() {
static vptyp::Logger l(std::cout);
if (!l.isConfigured()) {
l.configure({"size", "apprx", "garbage", "garbage2"});
}
@ -62,10 +62,9 @@ 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,16 +94,18 @@ static void BM_taylor_atomic_upd(benchmark::State& state) {
}
void updMapValue(double res, const std::string& key) {
static std::unordered_map<std::string, double> d = {
{"key", 0}, {"assembly", 0}, {"draw", 0.0}, {"d2", 0.0}, {"d55", 0.0}};
static std::unordered_map<std::string, double> d = {{"key", 10.0},
{"assembly", 11.0},
{"draw", 123.3},
{"d2", 0.0},
{"d55", 0.23}};
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");
}
}
@ -134,13 +135,12 @@ 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 < alphabetSize && c - small >= 0) ||
(c - big < alphabetSize && c - big >= 0));
int id{0}, begin{0};
assert((c - small < 25 && c - small >= 0) ||
(c - big < 25 && c - big >= 0));
int id, begin;
if (c - small < 0) {
id = c - big;
begin = big;
@ -148,13 +148,12 @@ std::string caesar_encoder(const std::string& input) {
id = c - small;
begin = small;
}
encoded += begin + (id + 3) % alphabetSize;
encoded += begin + (id + 3) % 25;
}
return encoded;
}
static constexpr int sizeDef = 1000;
int caesar_base(int size = sizeDef) {
int caesar_base(int size = 1000) {
std::string a(size, 'a');
auto res = caesar_encoder(a);
return size;

View File

@ -1,7 +1,6 @@
[requires]
gtest/1.16.0
rapidyaml/0.9.0
glog/0.7.1-unwindfix
glog/0.7.1
benchmark/1.9.1
boost/1.84.0
@ -14,4 +13,6 @@ boost/*:without_* = True
[generators]
CMakeDeps
CMakeToolchain
CMakeToolchain
[layout]
cmake_layout

View File

@ -13,28 +13,29 @@ namespace vptyp {
static constexpr std::string_view configErrorMsg =
"Bruh, incorrect configuration";
class MetricsLogger {
class Logger {
// helper class for handling worker thread
class Worker;
using map_type =
std::unordered_map<std::string, std::variant<int64_t, double>>;
public:
virtual ~MetricsLogger();
MetricsLogger();
explicit MetricsLogger(std::ostream& out);
MetricsLogger(MetricsLogger&) = delete;
MetricsLogger(MetricsLogger&&) = delete;
MetricsLogger& operator=(MetricsLogger&) = delete;
MetricsLogger& operator=(MetricsLogger&&) = delete;
virtual ~Logger();
Logger();
explicit Logger(std::ostream& out);
Logger(Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&) = delete;
Logger& operator=(Logger&&) = 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<std::string>& d);
template <typename Metric>
void add(const std::string& field, Metric metric)
requires(std::is_arithmetic_v<Metric>)
{
template <typename Metric,
typename = std::enable_if_t<std::is_arithmetic_v<Metric>>>
void add(const std::string& field, Metric metric) {
refs.fetch_add(1, std::memory_order_release);
map_type* locked = active.load(std::memory_order_acquire);
auto it = locked->find(field);
@ -49,16 +50,14 @@ class MetricsLogger {
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<int> configured{NOT_CONFIGURED};
std::unique_ptr<Worker> worker;
std::unique_ptr<map_type> m1, m2;
std::atomic<map_type*> active;
std::atomic<size_t> refs{0}; // degradation on worker side (waiting for no
// one to be in refs section)
std::atomic<map_type*> active; // impl may use mutex!
std::atomic<size_t> refs{0}; // degradation on worker side (waiting for no
// one to be in refs section)
};
} // namespace vptyp

122
src/logger.cc Normal file
View File

@ -0,0 +1,122 @@
#include "logger.hh"
#include <atomic>
#ifdef INLOOP_TIME
#include <chrono>
#endif
#include <iostream>
#include <memory>
#include <thread>
namespace vptyp {
bool Logger::configure(const std::vector<std::string>& 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<map_type>()), m2(std::make_unique<map_type>()) {
worker = std::make_unique<Logger::Worker>(*this, std::cout);
}
Logger::Logger(std::ostream& out)
: m1(std::make_unique<map_type>()), m2(std::make_unique<map_type>()) {
worker = std::make_unique<Logger::Worker>(*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();
std::this_thread::yield();
}
}
void unroll();
private:
enum State { UNDEF, RUNNING, STOPPING };
std::atomic<State> 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
#ifdef INLOOP_TIME
auto start_time = std::chrono::high_resolution_clock::now();
#endif
while (parent.refs.load(std::memory_order_acquire) > 0) {
std::this_thread::yield();
}
#ifdef INLOOP_TIME
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
end_time - start_time);
static std::atomic<int64_t> max_wait_time{0};
int64_t current_time = duration.count();
int64_t current_max = max_wait_time.load();
if (current_time > current_max &&
max_wait_time.compare_exchange_weak(current_max, current_time)) {
std::cout << "New maximum time spent waiting in while loop: "
<< current_time << " nanoseconds" << std::endl;
}
#endif
// 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

View File

@ -1,147 +0,0 @@
#include <unistd.h>
#include <atomic>
#include <format>
#include <iomanip>
#include <memory>
#include <metricsLogger.hh>
#include <thread>
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<std::string>& 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<map_type>()), m2(std::make_unique<map_type>()) {
worker = std::make_unique<MetricsLogger::Worker>(*this, std::cout);
}
MetricsLogger::MetricsLogger(std::ostream& out)
: m1(std::make_unique<map_type>()), m2(std::make_unique<map_type>()) {
worker = std::make_unique<MetricsLogger::Worker>(*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> 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

View File

@ -1,7 +1,4 @@
enable_testing()
find_package(GTest REQUIRED)
include(GoogleTest)
find_package(Boost REQUIRED COMPONENTS thread)
@ -14,6 +11,6 @@ target_link_libraries(${NAME} PRIVATE
Boost::thread
${PROJECT_NAME}
)
gtest_discover_tests(${NAME})
# target_compile_options(${NAME} PRIVATE -fsanitize=thread)
# target_link_options(${NAME} PRIVATE -fsanitize=thread)
target_compile_options(${NAME} PRIVATE -fsanitize=thread)
target_link_options(${NAME} PRIVATE -fsanitize=thread)

View File

@ -1,21 +1,21 @@
#include <gtest/gtest.h>
#include <boost/lockfree/queue.hpp>
#include <metricsLogger.hh>
#include <logger.hh>
#include <queue>
#include <thread>
TEST(SingleThread, Configuration) {
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));
}
// TEST(SingleThread, Configuration) {
// vptyp::Logger l;
// EXPECT_NO_THROW(l.configure({"apprx", "size", "time"}));
// EXPECT_NO_THROW(l.add("apprx", 123));
// EXPECT_ANY_THROW(l.add("not in configuration", 123));
// }
void outBufferCheck(std::ostringstream& s,
std::queue<std::pair<std::string, int64_t>>& prev) {
std::string_view outBufferView = s.rdbuf()->view();
std::string_view lineView;
size_t startPos = 0;
auto startPos = 0;
while (startPos < outBufferView.length() &&
(lineView = outBufferView.substr(startPos, outBufferView.find('\n')),
lineView.size() > 0)) {
@ -33,81 +33,115 @@ void outBufferCheck(std::ostringstream& s,
s.emplace(nextAwaiting.first);
prev.pop();
}
std::cout << nextAwaiting.first << std::endl;
// std::cout << nextAwaiting.first << std::endl;
}
std::cout << "n" << std::endl;
// std::cout << "n" << std::endl;
}
EXPECT_EQ(prev.empty(), true);
}
TEST(SingleThread, Add) {
// TEST(SingleThread, Add) {
// std::ostringstream s;
// std::queue<std::pair<std::string, int64_t>> prev;
// {
// vptyp::Logger l(s);
// l.configure({"apprx", "size", "time"});
// auto decorator = [&l, &prev](std::string field, int value) {
// l.add(field, value);
// prev.push({field, value});
// };
// decorator("size", 1);
// decorator("time", 1);
// sleep(2); // twice of logger sleep
// decorator("apprx", 12);
// decorator("size", 2);
// sleep(2);
// }
// // results
// outBufferCheck(s, prev);
// }
// TEST(MultiThread, Configure) {
// vptyp::Logger l(std::cout);
// std::atomic<size_t> howManyConfigured;
// auto fConfig = [&l, &howManyConfigured] {
// bool res{false};
// EXPECT_NO_THROW(res = l.configure({"apprx", "size", "time"}));
// howManyConfigured.fetch_add(static_cast<size_t>(res));
// };
// // simulate race condition configure
// std::vector<std::jthread> threads(10);
// for (auto& thread : threads) {
// thread = std::jthread(fConfig);
// }
// threads.clear();
// EXPECT_EQ(howManyConfigured.load(), 1);
// }
TEST(MultiThread, Add) {
std::ostringstream s;
std::queue<std::pair<std::string, int64_t>> prev;
std::mutex mtx;
{
vptyp::MetricsLogger l(s);
vptyp::Logger l(s);
l.configure({"apprx", "size", "time"});
auto decorator = [&l, &prev](std::string field, int value) {
l.add(field, value);
prev.emplace(field, value);
auto decorator = [&l, &prev, &mtx](std::string field, int value) {
for (auto i = 0; i < 1 << 20; ++i) {
l.add(field, value);
{
std::lock_guard g(mtx);
prev.push({field, value});
}
std::this_thread::yield();
}
};
decorator("size", 1);
decorator("time", 1);
sleep(2); // twice of logger sleep
decorator("apprx", 1);
decorator("size", 2);
sleep(2);
std::vector<std::jthread> 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.clear();
sleep(1);
}
// results
outBufferCheck(s, prev);
}
TEST(MultiThread, Configure) {
static constexpr size_t threadsNumber = 10;
vptyp::MetricsLogger l(std::cout);
std::atomic<size_t> howManyConfigured;
auto fConfig = [&l, &howManyConfigured] {
bool res{false};
EXPECT_NO_THROW(res = l.configure({"apprx", "size", "time"}));
howManyConfigured.fetch_add(static_cast<size_t>(res));
};
// simulate race condition configure
std::vector<std::jthread> threads(threadsNumber);
for (auto& thread : threads) {
thread = std::jthread(fConfig);
}
threads.clear();
EXPECT_EQ(howManyConfigured.load(), 1);
}
TEST(MultiThread, Add) {
static constexpr size_t maxRandNumber = 100;
static constexpr size_t producerIterations = 5;
TEST(MultiThread, Add16Threads) {
std::ostringstream s;
std::queue<std::pair<std::string, int64_t>> prev;
std::mutex mtx;
{
vptyp::MetricsLogger l(s);
l.configure({"apprx", "size", "time"});
vptyp::Logger l(s);
l.configure({"field0", "field1", "field2", "field3", "field4", "field5",
"field6", "field7", "field8", "field9", "field10", "field11",
"field12", "field13", "field14", "field15"});
auto decorator = [&l, &prev, &mtx](std::string field, int value) {
for (size_t i = 0; i < producerIterations; ++i) {
for (auto i = 0; i < 1 << 20; ++i) {
l.add(field, value);
{
std::lock_guard g(mtx);
prev.emplace(field, value);
prev.push({field, value});
}
sleep(1);
std::this_thread::yield();
}
};
std::vector<std::jthread> threads(3);
threads[0] = std::jthread(decorator, "apprx", rand() % maxRandNumber);
threads[1] = std::jthread(decorator, "size", rand() % maxRandNumber);
threads[2] = std::jthread(decorator, "time", rand() % maxRandNumber);
std::vector<std::jthread> threads(16);
// Create 16 threads, each with its own unique field
for (int i = 0; i < 16; ++i) {
std::string field = "field" + std::to_string(i);
threads[i] = std::jthread(decorator, field, rand() % 100);
}
threads.clear();
sleep(1);