Skip to content

Commit 8e3e766

Browse files
committed
feat: first wokring benchmark :) , housekeeping
1 parent 7f7e829 commit 8e3e766

19 files changed

Lines changed: 469 additions & 52 deletions

.vscode/launch.json

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,31 @@
3939
"args": [],
4040
"stopAtEntry": false,
4141
"cwd": "${workspaceFolder}",
42-
"environment": [],
43-
"envFile": "${workspaceFolder}/.env",
42+
"externalConsole": false,
43+
"MIMode": "gdb",
44+
"setupCommands": [
45+
{
46+
"description": "Enable pretty-printing for gdb",
47+
"text": "-enable-pretty-printing",
48+
"ignoreFailures": true
49+
},
50+
{
51+
"description": "Set Disassembly Flavor to Intel",
52+
"text": "-gdb-set disassembly-flavor intel",
53+
"ignoreFailures": true
54+
}
55+
],
56+
"preLaunchTask": "build",
57+
"miDebuggerPath": "/usr/bin/gdb"
58+
},
59+
{
60+
"name": "benchmark-debug",
61+
"type": "cppdbg",
62+
"request": "launch",
63+
"program": "${workspaceFolder}/build/Debug/benchmarks/benchmarks",
64+
"args": [],
65+
"stopAtEntry": false,
66+
"cwd": "${workspaceFolder}",
4467
"externalConsole": false,
4568
"MIMode": "gdb",
4669
"setupCommands": [

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"isDefault": true
1515
},
1616
"problemMatcher": [],
17-
"detail": "Builds the project using Conan and CMake Debug preset"
17+
"detail": "Builds the project using Conan, and the CMake 'Debug' preset. does not `rm -rf` build"
1818
}
1919
]
2020
}

CMakeLists.txt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,8 @@ add_library(traderlib STATIC ${SOURCES})
2323

2424
# === Conan Setup ===============================
2525

26-
# Conan-generated toolchain setup
27-
include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake OPTIONAL RESULT_VARIABLE _found_toolchain)
28-
if(_found_toolchain)
29-
message(STATUS "Conan toolchain included")
30-
endif()
31-
3226
find_package(absl REQUIRED)
27+
find_package(benchmark REQUIRED)
3328
find_package(Boost REQUIRED)
3429
find_package(concurrentqueue REQUIRED)
3530
find_package(efsw REQUIRED)
@@ -44,6 +39,7 @@ find_package(spdlog REQUIRED)
4439

4540
target_link_libraries(traderlib PUBLIC
4641
abseil::abseil
42+
benchmark::benchmark_main
4743
boost::boost
4844
concurrentqueue::concurrentqueue
4945
efsw::efsw
@@ -58,13 +54,18 @@ target_link_libraries(traderlib PUBLIC
5854
)
5955

6056

61-
# === Create main executable ====================
57+
# === Main executable ===========================
6258

6359
add_executable(tradercpp "src/main.cpp")
6460
target_link_libraries(tradercpp PRIVATE traderlib)
6561

6662

67-
# === Enable `ctest` ============================
63+
# === Unit test executable ======================
6864

6965
enable_testing()
7066
add_subdirectory(tests)
67+
68+
69+
# === Benchmark executable ======================
70+
71+
add_subdirectory(benchmarks)

CMakePresets.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@
5252
"output": {
5353
"outputOnFailure": true
5454
}
55+
},
56+
{
57+
"name": "release",
58+
"configurePreset": "release",
59+
"output": {
60+
"outputOnFailure": true
61+
}
5562
}
5663
]
5764
}

Makefile

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,26 @@ init:
2626
python3 -m venv .venv
2727
source .venv/bin/activate && \
2828
pip install gcovr conan && \
29-
conan install . --lockfile=conan.lock --build=missing -s build_type=Debug
29+
conan install . --lockfile=conan.lock --build=missing -s build_type=Debug && \
30+
conan install . --lockfile=conan.lock --build=missing -s build_type=Release
3031
cmake --preset=debug
3132

32-
## lock-conan: 📦 run after installing conan dependencies
33+
## lock-conan: 📦 run after adding (but before installing) conan dependencies
3334
.PHONY: lock-conan
3435
lock-conan:
35-
conan lock create . --profile:host=default -s build_type=Debug --lockfile-out=conan.lock
36+
conan lock create . --profile:host=default -s build_type=Debug --lockfile-out=conan.lock
3637
conan lock create . --profile:host=default -s build_type=Release --lockfile=conan.lock --lockfile-out=conan.lock
3738

3839
## build-debug: 🔨 compile (debug)
3940
.PHONY: build-debug
4041
build-debug:
4142
$(call pp,assuming `make init` has been called)
43+
cmake --preset=debug
4244
cmake --build --preset=debug
4345

4446
## build-release: 🏎️ compile (prod)
4547
.PHONY: build-release
4648
build-release:
47-
source .venv/bin/activate && \
48-
conan install . --lockfile=conan.lock --build=missing -s build_type=Release
4949
cmake --preset=release
5050
cmake --build --preset=release
5151

@@ -59,10 +59,17 @@ test:
5959
source .venv/bin/activate && \
6060
gcovr -r . --exclude 'tests/*' --sonarqube -o sonar-coverage.xml
6161

62+
## bench: ⏱️ build and run benchmarks
63+
.PHONY: bench
64+
bench:
65+
$(call pp,assuming `make build-release` has been called)
66+
cmake --build --preset release
67+
build/Release/benchmarks/benchmarks --benchmark_report_aggregates_only=false
68+
6269
## tidy: 🧹 tidy things up before committing code
6370
.PHONY: tidy
6471
tidy:
65-
find src/ tests/ \( -name '*.cpp' -o -name '*.hpp' -o -name '*.c' -o -name '*.h' \) -exec clang-format -i {} +
72+
find src/ tests/ benchmarks/ \( -name '*.cpp' -o -name '*.hpp' -o -name '*.c' -o -name '*.h' \) -exec clang-format -i {} +
6673
hooks/check_clang_tidy.sh
6774

6875
## run-debug: 🏃‍♂️ run the app (debug) (don't forget `withenv`)

README.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -141,48 +141,51 @@ NB: this app uses `make` as a recipe book, but it's not essential:
141141
- ✅ dependency injection
142142
- integration test with mocked Binance server
143143
- FTXUI snapshot testing
144+
- benchmarking
145+
- micro benchmarks
146+
- load test with mocked FIX server
147+
- profiling (valgrind/cachegrind)
148+
- profile-guided optimization (pgo)
149+
- profile tcmalloc
144150
- performance / latency
145151
- ✅ store prices and sizes as integrals (ticks as `uint64_t`) for performance
146152
- ✅ cache line alignment
147153
- ✅ tcmalloc (Full) / gperftools
148-
- profiling tcmalloc
149-
- release compile flags
150-
- profiling (valgrind/cachegrind)
151-
- profile-guided optimization (pgo)
152-
- load test with mocked FIX server
153154
- CPU
154155
- ✅ process priority
155156
- ✅ FIX-thread "realtime"
156157
- ✅ FIX-thread CPU affinity
157158
- Disable hyperthreading
159+
- OS
160+
- ✅ vacate OS services
161+
- ✅ move IRQs for all system devices to other CPUs
162+
- RTOS / PREEMPT_RT kernel
163+
- co-location
164+
- ✅ find Binance's server location for a low-latency connection
158165
- NIC
159166
- ✅ NIC IRQ affinity to same CPU as FIX
160167
- wired, kernel-bypass NICs
161168
- hardware queue affinity
162169
- QoS (mark packets)
163170
- AF_XDP (+ Zero-copy mode)
164171
- ~dedicated NIC + DPDK~
165-
- OS
166-
- ✅ vacate OS services
167-
- move IRQs for all system devices to other CPUs
168-
- RTOS / PREEMPT_RT kernel
172+
- FIX
173+
- ✅ debug quickfix to confirm if it's running in it's own thread
174+
- QuickFIX alternative (Fix8)
175+
- otherwise => QuickFIX + SSL
176+
- kernel space vs user space
177+
- sparse arrays & flat matrix
178+
- release compile flags
179+
- memory-mapped files
180+
- Memory locking
169181
- BIOS
170182
- disable hyperthreading, turbo boost
171183
- disable C-states deeper than C1 (C1E, C6, etc)
172184
- set cpu governor to "performance"
173-
- Memory locking
174-
- sparse arrays & flat matrix
175-
- memory-mapped files
176-
- (analyse) find Binance's server location for a low-latency connection
177-
- (analyse) how to quantify latency?
178-
- FIX SSL connectivity, to avoid stunnel latency overhead
179-
- QuickFIX alternative (Fix8)
180-
- kernel space vs user space
181-
- RT OS
182185
- ✅ logging
183186
- ✅ fast
184-
- add console target for fatal messages
185-
- error handling
187+
- ✅ error handling
188+
- ✅ add console target for fatal messages
186189
- compiled out 'debug' logging for release builds
187190
- thread name in logs
188191
- rolling
@@ -196,9 +199,6 @@ NB: this app uses `make` as a recipe book, but it's not essential:
196199
- observability
197200
- opentelemetry (asynchronous)
198201
- grafana+tempo via docker-compose
199-
- FIX
200-
- ✅ debug quickfix to confirm if it's running in it's own thread
201-
- switch to Fix8
202202
- security
203203
- OpenSSF Scorecard
204204
- other

benchmarks/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
file(GLOB_RECURSE BENCHMARK_SOURCES
2+
"${CMAKE_CURRENT_SOURCE_DIR}/*.cpp"
3+
)
4+
5+
add_executable(benchmarks ${BENCHMARK_SOURCES})
6+
7+
target_include_directories(benchmarks PRIVATE
8+
${CMAKE_SOURCE_DIR}/src
9+
)
10+
11+
target_link_libraries(benchmarks PRIVATE
12+
traderlib # Core library
13+
abseil::abseil
14+
benchmark::benchmark
15+
spdlog::spdlog
16+
)
17+
18+
add_test(NAME benchmarks COMMAND benchmarks)

benchmarks/main.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#include <benchmark/benchmark.h>
2+
3+
#include <cstdlib>
4+
5+
#include "spdlog/spdlog.h"
6+
#include "utils/logging.h"
7+
8+
struct Initializer {
9+
Initializer() {
10+
setenv("LOG_PATH", "logs/benchmark", 1);
11+
setenv("LOG_LEVEL", "info", 1);
12+
utils::Logging::configure();
13+
}
14+
};
15+
static Initializer init;
16+
17+
BENCHMARK_MAIN();
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#include <benchmark/benchmark.h>
2+
#include <fmt/ranges.h>
3+
4+
#include <random>
5+
6+
#include "core/bid_ask.h"
7+
#include "core/order_book.h"
8+
#include "spdlog/spdlog.h"
9+
10+
class OrderBookFixture : public benchmark::Fixture {
11+
public:
12+
void SetUp([[maybe_unused]] const benchmark::State& state) override {
13+
// deterministically build an initial, fully-populated, order book
14+
book_ = std::make_unique<core::OrderBook>(bids_, asks_);
15+
16+
// bids
17+
uint64_t bid_px = MID_PRICE - 1;
18+
for (uint64_t i = 0; i <= DEPTH_LEVELS; ++i) {
19+
auto msg = FIX44::MarketDataIncrementalRefresh();
20+
auto change = FIX44::MarketDataIncrementalRefresh::NoMDEntries();
21+
change.set(FIX::Symbol("BTCUSDT"));
22+
change.set(FIX::MDUpdateAction(FIX::MDUpdateAction_NEW));
23+
change.set(FIX::MDEntryType(FIX::MDEntryType_BID));
24+
change.set(FIX::MDEntryPx(bid_px--));
25+
if (bid_px < MID_PRICE - DEPTH_LEVELS) {
26+
bid_px = MID_PRICE - 1;
27+
}
28+
change.set(FIX::MDEntrySize(i));
29+
msg.addGroup(change);
30+
book_->apply_increment(msg, false);
31+
}
32+
33+
// offers
34+
uint64_t ask_px = MID_PRICE + 1;
35+
for (uint64_t i = 0; i <= DEPTH_LEVELS; ++i) {
36+
auto msg = FIX44::MarketDataIncrementalRefresh();
37+
auto change = FIX44::MarketDataIncrementalRefresh::NoMDEntries();
38+
change.set(FIX::Symbol("BTCUSDT"));
39+
change.set(FIX::MDUpdateAction(FIX::MDUpdateAction_NEW));
40+
change.set(FIX::MDEntryType(FIX::MDEntryType_OFFER));
41+
change.set(FIX::MDEntryPx(ask_px++));
42+
if (ask_px > MID_PRICE + DEPTH_LEVELS) {
43+
ask_px = MID_PRICE + 1;
44+
}
45+
change.set(FIX::MDEntrySize(i));
46+
msg.addGroup(change);
47+
book_->apply_increment(msg, false);
48+
49+
// asks_[MID_PRICE + i] = 1;
50+
}
51+
}
52+
53+
void TearDown([[maybe_unused]] const benchmark::State& state) override {
54+
const auto vec = book_->to_vector();
55+
spdlog::info("Levels: [{}]", fmt::join(vec, ", "));
56+
}
57+
58+
// order book
59+
std::unique_ptr<core::OrderBook> book_;
60+
absl::btree_map<uint64_t, uint64_t, std::greater<>> bids_;
61+
absl::btree_map<uint64_t, uint64_t> asks_;
62+
//
63+
static constexpr uint64_t DEPTH_LEVELS = 500;
64+
static constexpr uint64_t MID_PRICE = 100'000;
65+
};
66+
67+
BENCHMARK_DEFINE_F(OrderBookFixture, BM_AddOrder)(benchmark::State& state) {
68+
FIX44::MarketDataIncrementalRefresh msg;
69+
FIX44::MarketDataIncrementalRefresh::NoMDEntries change;
70+
uint8_t tick_tock = 0;
71+
uint64_t volume = 1;
72+
uint64_t bid_px = MID_PRICE - 1;
73+
uint64_t ask_px = MID_PRICE + 1;
74+
for (auto _ : state) {
75+
msg = FIX44::MarketDataIncrementalRefresh();
76+
change = FIX44::MarketDataIncrementalRefresh::NoMDEntries();
77+
change.set(FIX::Symbol("BTCUSDT"));
78+
change.set(FIX::MDUpdateAction(FIX::MDUpdateAction_NEW));
79+
if (tick_tock == 0) {
80+
change.set(FIX::MDEntryType(FIX::MDEntryType_BID));
81+
change.set(FIX::MDEntryPx(bid_px--));
82+
if (bid_px < MID_PRICE - DEPTH_LEVELS) {
83+
bid_px = MID_PRICE - 1;
84+
}
85+
change.set(FIX::MDEntrySize(volume));
86+
tick_tock = 1;
87+
} else {
88+
change.set(FIX::MDEntryType(FIX::MDEntryType_OFFER));
89+
change.set(FIX::MDEntryPx(ask_px++));
90+
if (ask_px > MID_PRICE + DEPTH_LEVELS) {
91+
ask_px = MID_PRICE + 1;
92+
}
93+
change.set(FIX::MDEntrySize(volume++));
94+
tick_tock = 0;
95+
}
96+
msg.addGroup(change);
97+
book_->apply_increment(msg, false);
98+
}
99+
100+
state.counters["Updates/sec"] =
101+
benchmark::Counter(state.iterations(), benchmark::Counter::kIsRate);
102+
}
103+
104+
BENCHMARK_REGISTER_F(OrderBookFixture, BM_AddOrder)->Iterations(500'000);

0 commit comments

Comments
 (0)