What it does
The code creates a logger class which instantiates a circular buffer at construction and uses producer-consumer style approach using condition_variable to log and print the messages to stdout.
Code
logger.h
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <iomanip>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <stdio.h>
#include <string>
#include <sys/time.h>
#include <thread>
#include <utility>
#include <vector>
namespace TIME
{
void get_time(char*);
} // namespace TIME
class my_logger
{
static constexpr size_t TIME_BUF_SIZE = 19;
static constexpr size_t MESSAGE_BUFFER_SIZE = 300;
static constexpr size_t BUFFER_SIZE = 500;
static constexpr size_t MESSAGE_PRINT_THRESHOLD = 450;
static_assert(MESSAGE_PRINT_THRESHOLD < BUFFER_SIZE,
"Message print threshold must be smaller than msg buffer size");
std::vector<std::array<char, MESSAGE_BUFFER_SIZE>> m_to_print;
size_t m_head;
size_t m_tail;
std::mutex mu_buffer;
std::condition_variable m_consumer_cond_var;
std::condition_variable m_producer_cond_var;
std::atomic<bool> m_continue_logging;
std::thread m_printing_thread;
public:
my_logger();
~my_logger();
void logging_thread();
size_t msg_count() const
{
if(m_tail >= m_head)
return m_tail - m_head;
return m_tail + BUFFER_SIZE - m_head;
}
template <typename... Args>
void log(const char* format, const char msg_type[], Args... args)
{
std::unique_lock lg(mu_buffer);
// m_producer_cond_var.wait(lg, [this] { return msg_count() <= MESSAGE_PRINT_THRESHOLD; });
auto buf_ptr = m_to_print[m_tail].data();
const size_t N = std::strlen(msg_type);
TIME::get_time(buf_ptr);
std::snprintf(buf_ptr + TIME_BUF_SIZE - 1, N + 1, "%s", msg_type);
std::snprintf(buf_ptr + TIME_BUF_SIZE + N - 1,
MESSAGE_BUFFER_SIZE - N - TIME_BUF_SIZE + 1,
format,
std::forward<Args>(args)...);
m_tail = (m_tail + 1) % BUFFER_SIZE;
if(msg_count() > MESSAGE_PRINT_THRESHOLD)
{
lg.unlock();
m_consumer_cond_var.notify_one(); // notify the single consumer
}
}
template <typename... Args>
void info(const char* format, Args... args)
{
log(format, " [INFO] ", std::forward<Args>(args)...);
}
template <typename... Args>
void warn(const char* format, Args... args)
{
log(format, " [WARN] ", std::forward<Args>(args)...);
}
template <typename... Args>
void error(const char* format, Args... args)
{
log(format, " [ERROR] ", std::forward<Args>(args)...);
}
};
logger.cpp
#include "logger.h"
#include <algorithm>
#include <mutex>
namespace TIME
{
void get_time(char* buf)
{
auto currentTime = std::chrono::high_resolution_clock::now();
auto nanoSeconds = std::chrono::time_point_cast<std::chrono::nanoseconds>(currentTime);
auto nanoSecondsCount = nanoSeconds.time_since_epoch().count();
// Convert nanoseconds to seconds and fractional seconds
auto fracSeconds = nanoSecondsCount % 1'000'000'000;
// Convert seconds to std::time_t
std::time_t time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
// Convert time to struct tm in local time zone
std::tm* tm = std::localtime(&time);
std::strftime(buf, 9, "%H:%M:%S.", std::localtime(&time));
std::snprintf(buf + 9, 10, "%09lld", fracSeconds);
}
} // namespace TIME
my_logger::my_logger()
: m_head(0)
, m_tail(0)
, m_continue_logging(true)
{
m_to_print.assign(BUFFER_SIZE, {});
m_printing_thread = std::thread(&my_logger::logging_thread, this);
}
void my_logger::logging_thread()
{
while(m_continue_logging)
{
std::unique_lock lg(mu_buffer);
m_consumer_cond_var.wait(lg, [this] { return m_head != m_tail; });
while(m_head != m_tail)
{
std::printf("%s\n", m_to_print[m_head].data());
m_head = (m_head + 1) % BUFFER_SIZE;
}
lg.unlock();
m_producer_cond_var.notify_all(); //notify all producers
}
}
my_logger::~my_logger()
{
m_continue_logging = false;
m_consumer_cond_var.notify_one();
if(m_printing_thread.joinable())
m_printing_thread.join();
}
Benchmarking code
#include "logger.h"
#include <chrono>
#include <fstream>
#include <string>
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: logger <file>";
return 1;
}
constexpr unsigned RUNS = 33333;
const std::string benchmark_file_path = argv[1];
std::fstream file(benchmark_file_path, std::ios::out | std::ios::app);
if(!file.is_open())
{
std::cerr << "Couldn't open " << benchmark_file_path << "\n";
return 1;
}
auto start = std::chrono::high_resolution_clock::now();
{
my_logger logger;
for(size_t i = 0; i < RUNS; i++)
{
logger.info("this is a %s string with id: %u and double: %f",
"sundar",
i,
static_cast<double>(i + 0.5));
logger.warn("this is a %s string with id: %u and double: %f",
"WARN",
2 * i,
static_cast<double>(2 * i));
logger.error("this is a %s string with id: %u and double: %f",
"ERROR",
4 * i,
static_cast<double>(4 * i));
}
}
auto finish = std::chrono::high_resolution_clock::now();
auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(finish - start).count();
file << "RUNS: " << 3 * RUNS << " TIME_MULTITHREADED: " << time1 << " ms\n";
start = std::chrono::high_resolution_clock::now();
{
char time_buf[19];
for(size_t i = 0; i < RUNS; i++)
{
TIME::get_time(time_buf);
fprintf(stdout,
"%s"
" [INFO] "
"this is a %s string with id: %zu and double: %f\n",
time_buf,
"atisundar",
i,
static_cast<double>(i + 0.5));
TIME::get_time(time_buf);
fprintf(stdout,
"%s"
" [WARN] "
"this is a %s string with id: %zu and double: %f\n",
time_buf,
"atisundar",
i,
static_cast<double>(2 * i));
TIME::get_time(time_buf);
fprintf(stdout,
"%s"
" [ERROR] "
"this is a %s string with id: %zu and double: %f\n",
time_buf,
"atisundar",
i,
static_cast<double>(4 * i));
}
}
finish = std::chrono::high_resolution_clock::now();
auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(finish - start).count();
file << "RUNS: " << 3 * RUNS << " TIME_PLAIN_LOGGER: " << time2 << " ms\n";
return 0;
}
Current performance
RUNS: 99999 TIME_MULTITHREADED: 282 ms
RUNS: 99999 TIME_PLAIN_LOGGER: 970 ms
RUNS: 99999 TIME_MULTITHREADED: 203 ms
RUNS: 99999 TIME_PLAIN_LOGGER: 972 ms
RUNS: 99999 TIME_MULTITHREADED: 217 ms
RUNS: 99999 TIME_PLAIN_LOGGER: 970 ms
Known issues
The log function in logger.h isn't thread safe because when multiple threads are trying to log and one of them causes total messages to exceed MESSAGE_PRINT_THRESHOLD, then after the unlock step in log and before notifying the m_consumer_cond_var, one of the other logging threads might take ownership of the mutex and keep writing into the buffer and potentially lead to overwriting the circular buffer. The solution I can see for this is to uncomment the m_producer_cond_var.wait line in log function, but that is causing the code to become slower than plain logger function by almost 1.5x . Can you please suggest what I may be doing wrong here?