Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1aeeb10
feat(bench): add latency benchmarks
LimiNode Sep 18, 2025
3deaf98
ci: add gated bench job
LimiNode Sep 18, 2025
d402daa
ci: fix benchmark run path
LimiNode Sep 18, 2025
d22d791
refactor(bench): refine latency measurements
LimiNode Sep 18, 2025
500e8f5
fix(ci): point benchmark step to binary
LimiNode Sep 18, 2025
f00e844
fix(tests): guard tsan access in backpressure ordering
LimiNode Sep 18, 2025
08ce203
fix(bench): stabilise benchmark execution
LimiNode Sep 18, 2025
728c7a5
ci: throttle logit bench runtime
LimiNode Sep 18, 2025
f3f0fd2
ci(workflow): trust pull_request_target
LimiNode Sep 19, 2025
2f2247e
ci: handle internal pull requests
LimiNode Sep 19, 2025
cad3ed1
ci(workflow): drop unused write permissions
LimiNode Sep 19, 2025
cedd8a3
revert(ci): restore original workflow
LimiNode Sep 19, 2025
4c4d15c
revert(ci): restore bench throttle
LimiNode Sep 19, 2025
53426df
fix: stabilise benchmark execution
LimiNode Sep 19, 2025
08db148
fix(bench): add timestamped logs and watchdog progress
LimiNode Sep 19, 2025
1bfd685
fix(bench): refresh watchdog and extend timeout
LimiNode Sep 19, 2025
a9fd13c
fix(bench): refresh watchdog and extend timeout
LimiNode Sep 19, 2025
33b58e1
Merge branch 'codex/add-latency-and-throughput-benchmarks-for-logging…
LimiNode Sep 19, 2025
1061bc6
Codex/fix ci issue with logit bench execution 5dkzon
LimiNode Sep 19, 2025
79dffd5
refactor: added LOGIT_SET_MAX_QUEUE(total_messages)
LimiNode Sep 19, 2025
67d26cf
refactor: added <logit.hpp> in LogItAdapter.hpp
LimiNode Sep 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [ main ]
branches: [ main, stable ]
pull_request:
branches: [ main ]

Expand All @@ -25,6 +25,20 @@ jobs:
run: cmake --install build --prefix install
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Configure benchmarks
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/stable') }}
run: cmake -S . -B build-bench -DLOGIT_BENCH_ENABLE=ON -DLOGIT_BENCH_WITH_SPDLOG=ON -DCMAKE_CXX_STANDARD=${{ matrix.std }} -DLOGIT_WITH_SYSLOG=ON -DLOGIT_WITH_WIN_EVENT_LOG=OFF
- name: Build benchmarks
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/stable') }}
run: cmake --build build-bench --target logit_bench
- name: Run latency benchmarks
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/stable') }}
timeout-minutes: 20
env:
LOGIT_BENCH_TIMEOUT_SEC: 900
LOGIT_BENCH_TOTAL: 20000
LOGIT_BENCH_WARMUP: 2000
run: ./build-bench/logit_bench
- name: Configure consumer project
run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build consumer project
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ project(log-it-cpp VERSION 1.0.0 LANGUAGES CXX)

option(LOGIT_CPP_BUILD_TESTS "Build log-it-cpp tests" ${PROJECT_IS_TOP_LEVEL})
option(LOGIT_CPP_BUILD_EXAMPLES "Build log-it-cpp examples" OFF)
option(LOGIT_BENCH_ENABLE "Build log-it-cpp benchmarks" OFF)
option(LOGIT_BENCH_WITH_SPDLOG "Enable spdlog comparison benchmarks" OFF)
option(LOGIT_WITH_GZIP "Enable gzip via zlib" OFF)
option(LOGIT_WITH_ZSTD "Enable zstd" OFF)
option(LOGIT_WITH_FMT "Enable fmt support" OFF)
Expand Down Expand Up @@ -140,6 +142,10 @@ if(LOGIT_CPP_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()

if(LOGIT_BENCH_ENABLE)
add_subdirectory(bench)
endif()

include(CMakePackageConfigHelpers)

install(DIRECTORY include/ DESTINATION include)
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,20 @@ When building with Emscripten the library runs without threads. Console logging
works as usual while file-based loggers are replaced by stubs that warn when
used.

## Benchmarks

Latency and throughput benchmarks live under `bench/`. Enable them during configuration and optionally pull in the spdlog
adapters:

```bash
cmake -S . -B build -DLOGIT_BENCH_ENABLE=ON -DLOGIT_BENCH_WITH_SPDLOG=ON
cmake --build build --target logit_bench
```

Run `./build/bench/logit_bench` to record the full matrix (sync/async × null/file × producer counts × message sizes). Results
are appended to `bench/results/latency.csv` with one row per library/combination. Override the workload via `LOGIT_BENCH_TOTAL`
and `LOGIT_BENCH_WARMUP` environment variables if you need a lighter run.

---

## Documentation
Expand Down
39 changes: 39 additions & 0 deletions bench/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
set(LOGIT_BENCH_SOURCES
logit_bench.cpp
adapters/LogItAdapter.cpp
)

if(LOGIT_BENCH_WITH_SPDLOG)
list(APPEND LOGIT_BENCH_SOURCES adapters/SpdlogAdapter.cpp)
endif()

add_executable(logit_bench ${LOGIT_BENCH_SOURCES})

target_include_directories(logit_bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

target_compile_features(logit_bench PRIVATE cxx_std_17)

set_target_properties(logit_bench PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
)

foreach(config IN ITEMS DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
set_target_properties(logit_bench PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${config} ${CMAKE_BINARY_DIR}
)
endforeach()

target_link_libraries(logit_bench PRIVATE log-it-cpp::log-it-cpp)

if(LOGIT_BENCH_WITH_SPDLOG)
target_compile_definitions(logit_bench PRIVATE LOGIT_BENCH_HAVE_SPDLOG=1)
if(NOT TARGET spdlog::spdlog)
include(FetchContent)
FetchContent_Declare(spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.12.0
)
FetchContent_MakeAvailable(spdlog)
endif()
target_link_libraries(logit_bench PRIVATE spdlog::spdlog)
endif()
107 changes: 107 additions & 0 deletions bench/LatencyRecorder.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#pragma once

#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <vector>
#include <cmath>

namespace logit_bench {

/**
* Lock-free recorder for latency samples:
* - begin(record=true) returns a Token with an assigned slot and t0_ns (steady_clock).
* - complete(token) stores (t1-t0) in that slot.
* - finalize() returns p50/p99/p99.9 using nearest-rank (ceil) on a sorted copy.
*
* Thread-safety: concurrent writers store into distinct preallocated slots.
*/
class LatencyRecorder {
public:
struct Token {
std::uint64_t slot = invalid_slot();
std::uint64_t t0_ns = 0;
bool active = false;
};

struct Summary {
std::uint64_t p50_ns = 0;
std::uint64_t p99_ns = 0;
std::uint64_t p999_ns = 0;
};

explicit LatencyRecorder(std::size_t total)
: m_values(total),
m_expected(total),
m_next_slot(0) {}

/**
* Reserve a slot (if record==true) and capture t0 using steady_clock.
* We take t0 **after** the slot reservation to minimize skew before log().
*/
Token begin(bool record) {
Token token;
token.active = record;
if (record) {
const auto slot = m_next_slot.fetch_add(1, std::memory_order_relaxed);
if (slot >= m_expected) {
throw std::out_of_range("LatencyRecorder capacity exceeded");
}
token.slot = static_cast<std::uint64_t>(slot);
token.t0_ns = now();
}
return token;
}

/// Capture t1 and store (t1 - t0) into the reserved slot.
void complete(const Token& token) {
if (!token.active) return;
const auto t1_ns = now();
m_values[token.slot] = t1_ns - token.t0_ns; // distinct slots -> no data race
}

std::size_t recorded() const {
return m_next_slot.load(std::memory_order_relaxed);
}

Summary finalize() const {
if (recorded() != m_expected) {
throw std::runtime_error("Incomplete latency capture");
}
std::vector<std::uint64_t> sorted = m_values;
std::sort(sorted.begin(), sorted.end());
Summary summary;
summary.p50_ns = pick(sorted, 0.50);
summary.p99_ns = pick(sorted, 0.99);
summary.p999_ns = pick(sorted, 0.999);
return summary;
}

static std::uint64_t invalid_slot() {
return std::numeric_limits<std::uint64_t>::max();
}

static std::uint64_t now() {
const auto now_tp = std::chrono::steady_clock::now().time_since_epoch();
return std::chrono::duration_cast<std::chrono::nanoseconds>(now_tp).count();
}

private:
// Nearest-rank percentile with ceil(p * N), clamped to [0..N-1].
static std::uint64_t pick(const std::vector<std::uint64_t>& data, double p) {
if (data.empty()) return 0;
const double r = std::ceil(p * static_cast<double>(data.size()));
std::size_t idx = (r <= 1.0) ? 0 : static_cast<std::size_t>(r) - 1;
if (idx >= data.size()) idx = data.size() - 1;
return data[idx];
}

std::vector<std::uint64_t> m_values; // preallocated; no reallocation
const std::size_t m_expected; // total messages to record
std::atomic<std::size_t> m_next_slot;
};

} // namespace logit_bench
29 changes: 29 additions & 0 deletions bench/Scenario.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <cstddef>
#include <string>

namespace logit_bench {

enum class SinkKind {
Null,
File,
};

inline std::string sink_name(SinkKind sink) {
switch (sink) {
case SinkKind::Null: return "null";
case SinkKind::File: return "file";
}
return "unknown";
}

struct Scenario {
bool async = false;
SinkKind sink = SinkKind::Null;
std::size_t producers = 1;
std::size_t message_bytes = 0;
std::size_t total_messages = 0;
};

} // namespace logit_bench
23 changes: 23 additions & 0 deletions bench/adapters/ILoggerAdapter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <string_view>

#include "../LatencyRecorder.hpp"
#include "../Scenario.hpp"

namespace logit_bench {

class ILoggerAdapter {
public:
virtual ~ILoggerAdapter() = default;

virtual const char* library_name() const = 0;

virtual void prepare(const Scenario& scenario, LatencyRecorder& recorder) = 0;

virtual void log(const LatencyRecorder::Token& token, std::string_view message) = 0;

virtual void flush() = 0;
};

} // namespace logit_bench
Loading
Loading