xrpld
Loading...
Searching...
No Matches
benchmarks/libxrpl/nodestore/Backend.cpp
1#include <xrpl/nodestore/Backend.h>
2
3#include <xrpl/basics/base_uint.h>
4#include <xrpl/nodestore/NodeObject.h>
5#include <xrpl/nodestore/Types.h>
6
7#include <benchmark/benchmark.h>
8#include <benchmarks/libxrpl/nodestore/NodeStoreBench.h>
9
10#include <array>
11#include <cstddef>
12#include <cstdint>
13#include <functional>
14#include <memory>
15#include <string>
16#include <string_view>
17#include <utility>
18#include <vector>
19
20namespace xrpl::node_store {
21namespace {
22
23constexpr auto kPoolSizes = std::to_array<std::size_t>({1000, 10000, 100000});
24constexpr auto kThreadCounts = std::to_array<std::size_t>({1, 4, 8});
25constexpr std::size_t kBatchSize = 256;
26constexpr std::size_t kMissRatio = 5;
27
28constexpr std::string_view kNamePrefix = "BM_Backend_";
29constexpr std::string_view kNameSeparator = "/";
30
31struct RunState
32{
33 std::unique_ptr<BackendHarness> harness;
34 Batch present;
35 Batch recent;
36 std::vector<uint256> missing;
37 std::vector<std::size_t> shuffle;
38 std::size_t avgPayload = 0;
39
40 void
41 release()
42 {
43 harness.reset();
44 present = Batch{};
45 recent = Batch{};
46 missing = std::vector<uint256>{};
47 shuffle = std::vector<std::size_t>{};
48 avgPayload = 0;
49 }
50};
51
52struct SetupContext
53{
54 RunState& rs;
55 Backend& backend;
56 std::size_t poolSize;
57};
58
59struct IterateContext
60{
61 RunState& rs;
62 Backend& backend;
63 std::size_t index;
64 std::size_t poolSize;
65};
66
67struct Workload
68{
69 std::string_view name;
70 std::function<void(SetupContext const&)> setup;
71 std::function<void(IterateContext const&)> iterate;
72 bool reportBytes = false; // SetBytesProcessed from rs.avgPayload
73 bool clobber = true; // ClobberMemory after the loop (false for pure stores)
74 bool pinToPool = false; // pin iterations to one pool sweep instead of autotuning
75};
76
77// One store() per iteration. Iterations are pinned to one pool sweep (per
78// thread) so the index never wraps past the pool - otherwise NuDB::doInsert
79// swallows key_exists and the workload degenerates into duplicate-detection
80// no-ops.
81Workload const kInsert{
82 .name = "Insert",
83 .setup =
84 [](SetupContext const& ctx) {
85 ctx.rs.present = makePool(1, ctx.poolSize);
86 ctx.rs.avgPayload = averagePayload(ctx.rs.present);
87 },
88 .iterate =
89 [](IterateContext const& ctx) {
90 auto const& [rs, backend, index, poolSize] = ctx;
91 backend.store(rs.present[index % poolSize]);
92 },
93 .reportBytes = true,
94 .clobber = false,
95 .pinToPool = true,
96};
97
98// One fetch() of a present key (a hit) per iteration.
99Workload const kFetch{
100 .name = "Fetch",
101 .setup =
102 [](SetupContext const& ctx) {
103 ctx.rs.present = makePool(1, ctx.poolSize);
104 ctx.rs.avgPayload = averagePayload(ctx.rs.present);
105 prepopulate(ctx.backend, ctx.rs.present);
106 },
107 .iterate =
108 [](IterateContext const& ctx) {
109 auto const& [rs, backend, index, poolSize] = ctx;
110 std::shared_ptr<NodeObject> result;
111 backend.fetch(rs.present[index % poolSize]->getHash(), &result);
112 benchmark::DoNotOptimize(result);
113 },
114 .reportBytes = true,
115};
116
117// One fetch() of a never-stored key (a miss); the backend is left empty.
118Workload const kMissing{
119 .name = "Missing",
120 .setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
121 .iterate =
122 [](IterateContext const& ctx) {
123 auto const& [rs, backend, index, poolSize] = ctx;
124 std::shared_ptr<NodeObject> result;
125 backend.fetch(rs.missing[index % poolSize], &result);
126 benchmark::DoNotOptimize(result);
127 },
128};
129
130// 80% hits / 20% misses. The fetch index comes from a shuffle table so access
131// is random-like without per-iteration RNG cost; sequential `index % poolSize`
132// would be artificially cache-friendly to RocksDB's block cache.
133Workload const kMixed{
134 .name = "Mixed",
135 .setup =
136 [](SetupContext const& ctx) {
137 ctx.rs.present = makePool(1, ctx.poolSize);
138 ctx.rs.missing = makeMissingKeys(ctx.poolSize);
139 ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1);
140 prepopulate(ctx.backend, ctx.rs.present);
141 },
142 .iterate =
143 [](IterateContext const& ctx) {
144 auto const& [rs, backend, index, poolSize] = ctx;
145 std::shared_ptr<NodeObject> result;
146 auto const pick = rs.shuffle[index % poolSize];
147 if (index % kMissRatio == 0)
148 {
149 backend.fetch(rs.missing[pick], &result);
150 }
151 else
152 {
153 backend.fetch(rs.present[pick]->getHash(), &result);
154 }
155 benchmark::DoNotOptimize(result);
156 },
157};
158
159// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The
160// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item
161// it's about to store this iteration - which would give an all-miss-then-hit
162// step instead of a smooth ramp. The store walks sequentially so each recent
163// object is stored once.
164Workload const kWork{
165 .name = "Work",
166 .setup =
167 [](SetupContext const& ctx) {
168 ctx.rs.present = makePool(1, ctx.poolSize);
169 ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize);
170 ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2);
171 prepopulate(ctx.backend, ctx.rs.present);
172 },
173 .iterate =
174 [](IterateContext const& ctx) {
175 auto const& [rs, backend, index, poolSize] = ctx;
176 auto const slot = index % poolSize;
177 auto const pick = rs.shuffle[slot];
178
179 std::shared_ptr<NodeObject> historical;
180 backend.fetch(rs.present[pick]->getHash(), &historical);
181 benchmark::DoNotOptimize(historical);
182
183 std::shared_ptr<NodeObject> recent;
184 backend.fetch(rs.recent[pick]->getHash(), &recent);
185 benchmark::DoNotOptimize(recent);
186
187 backend.store(rs.recent[slot]);
188 },
189 .clobber = true,
190 .pinToPool = true,
191};
192
193auto
194makeRunner(Workload w, std::string cfg, std::shared_ptr<RunState> rs)
195{
196 return [w = std::move(w), cfg = std::move(cfg), rs = std::move(rs)](benchmark::State& state) {
197 auto const poolSize = static_cast<std::size_t>(state.range(0));
198 if (state.thread_index() == 0)
199 {
200 rs->harness = std::make_unique<BackendHarness>(cfg);
201 w.setup(
202 SetupContext{.rs = *rs, .backend = *rs->harness->backend, .poolSize = poolSize});
203 }
204
205 std::size_t index = state.thread_index();
206 for (auto _ : state)
207 {
208 w.iterate(
209 IterateContext{
210 .rs = *rs,
211 .backend = *rs->harness->backend,
212 .index = index,
213 .poolSize = poolSize});
214 index += state.threads();
215 }
216
217 if (w.clobber)
218 benchmark::ClobberMemory();
219
220 state.SetItemsProcessed(state.iterations());
221 if (w.reportBytes)
222 state.SetBytesProcessed(static_cast<std::int64_t>(state.iterations() * rs->avgPayload));
223
224 if (state.thread_index() == 0)
225 rs->release();
226 };
227}
228
229// Register workload `w` against backend `bc`, choosing the registration shape
230// from `w.pinToPool`.
231void
232registerWorkload(BackendConfig const& bc, Workload const& w)
233{
234 std::string const cfg = bc.config;
235 std::string name{kNamePrefix};
236 name += w.name;
237 name += kNameSeparator;
238 name += bc.name;
239
240 if (!w.pinToPool)
241 {
242 auto rs = std::make_shared<RunState>();
243 benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
244 ->RangeMultiplier(10)
245 ->Range(kPoolSizes.front(), kPoolSizes.back())
246 ->Threads(1)
247 ->Threads(4)
248 ->Threads(8)
249 ->UseRealTime();
250
251 return;
252 }
253
254 for (auto const poolSize : kPoolSizes)
255 {
256 for (auto const threads : kThreadCounts)
257 {
258 if (poolSize % threads != 0)
259 continue;
260
261 auto rs = std::make_shared<RunState>();
262 benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
263 ->Arg(poolSize)
264 ->Iterations(poolSize / threads)
265 ->Threads(static_cast<int>(threads))
266 ->UseRealTime();
267 }
268 }
269}
270
271// One storeBatch() of kBatchSize objects per iteration. Single-threaded:
272// Backend::storeBatch must not run concurrently with itself or store().
273// Iterations are pinned to the batch count so the index never wraps into
274// key_exists no-ops. Kept separate from Workload: batch slicing and the
275// per-batch item/byte accounting don't fit the thread-axis mold.
276void
277registerStoreBatch(BackendConfig const& bc)
278{
279 std::string const cfg = bc.config;
280 std::string name{kNamePrefix};
281 name += "StoreBatch";
282 name += kNameSeparator;
283 name += bc.name;
284 for (auto const poolSize : kPoolSizes)
285 {
286 auto const numBatches = poolSize / kBatchSize;
287 if (numBatches == 0)
288 continue;
289
290 auto rs = std::make_shared<RunState>();
291 benchmark::RegisterBenchmark(
292 name,
293 [rs, cfg](benchmark::State& state) {
294 auto const poolSize = static_cast<std::size_t>(state.range(0));
295 rs->harness = std::make_unique<BackendHarness>(cfg);
296 rs->present = makePool(1, poolSize);
297 rs->avgPayload = averagePayload(rs->present);
298 std::vector<Batch> const batches = sliceFixedBatches(rs->present, kBatchSize);
299 if (batches.empty())
300 {
301 state.SkipWithError("pool smaller than one batch");
302 return;
303 }
304
305 std::size_t index = 0;
306 for (auto _ : state)
307 {
308 rs->harness->backend->storeBatch(batches[index % batches.size()]);
309 ++index;
310 }
311
312 state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations() * kBatchSize));
313 state.SetBytesProcessed(
314 static_cast<std::int64_t>(state.iterations() * kBatchSize * rs->avgPayload));
315 rs->release();
316 })
317 ->Arg(poolSize)
318 ->Iterations(numBatches);
319 }
320}
321
322[[maybe_unused]] bool const kRegistered = [] {
323 auto const workloads = std::to_array({&kInsert, &kFetch, &kMissing, &kMixed, &kWork});
324 for (auto const& bc : backendConfigs())
325 {
326 for (auto const* w : workloads)
327 registerWorkload(bc, *w);
328
329 registerStoreBatch(bc);
330 }
331 return true;
332}();
333
334} // namespace
335} // namespace xrpl::node_store
T empty(T... args)
T make_shared(T... args)
T make_unique(T... args)
std::vector< std::size_t > makeShuffle(std::size_t size, std::uint64_t seed)
std::vector< Batch > sliceFixedBatches(Batch const &pool, std::size_t batchSize)
std::vector< BackendConfig > const & backendConfigs()
void prepopulate(Backend &backend, Batch const &objects)
std::vector< uint256 > makeMissingKeys(std::size_t count)
std::vector< std::shared_ptr< NodeObject > > Batch
A batch of NodeObjects to write at once.
Batch makePool(std::uint8_t prefix, std::size_t count, std::size_t start=0)
std::size_t averagePayload(Batch const &pool)
T size(T... args)