xrpld
Loading...
Searching...
No Matches
benchmarks/libxrpl/nodestore/Database.cpp
1#include <xrpl/nodestore/Database.h>
2
3#include <xrpl/basics/Blob.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/nodestore/NodeObject.h>
6#include <xrpl/nodestore/Types.h>
7
8#include <benchmark/benchmark.h>
9#include <benchmarks/libxrpl/nodestore/NodeStoreBench.h>
10
11#include <array>
12#include <cstddef>
13#include <cstdint>
14#include <functional>
15#include <memory>
16#include <string>
17#include <string_view>
18#include <utility>
19#include <vector>
20
21namespace xrpl::node_store {
22namespace {
23
24// Number of distinct objects pre-generated per run.
25constexpr std::size_t kDefaultPoolSize = 100000;
26
27// Async read threads the Database spawns. Unused by the synchronous fetch path
28// these benchmarks take; kept fixed so runs are comparable.
29constexpr int kReadThreads = 4;
30
31constexpr std::string_view kNamePrefix = "BM_Database_";
32constexpr std::string_view kNameSeparator = "/";
33
34struct RunState
35{
36 std::unique_ptr<DatabaseHarness> harness;
37 Batch present; // prefix-1 objects, eligible to be stored
38 Batch recent; // prefix-1 objects in the "future" key space
39 std::vector<uint256> missing; // prefix-2 keys that are never stored
40 std::vector<std::size_t> shuffle; // [0, poolSize) permutation for random-like access
41 std::size_t avgPayload = 0; // mean getData().size() over `present`
42};
43
44struct SetupContext
45{
46 RunState& rs;
47 Database& db;
48 std::size_t poolSize;
49};
50
51struct IterateContext
52{
53 RunState& rs;
54 Database& db;
55 std::uint32_t seq;
56 std::size_t index;
57 std::size_t poolSize;
58};
59
60struct Workload
61{
62 std::string_view name;
63 std::function<void(SetupContext const&)> setup;
64 std::function<void(IterateContext const&)> iterate;
65 bool reportBytes = false;
66 bool pinIterations = false;
67};
68
69void
70prepopulate(Database& db, Batch const& objects)
71{
72 auto const seq = db.earliestLedgerSeq();
73 for (auto const& obj : objects)
74 {
75 Blob data(obj->getData());
76 db.store(obj->getType(), std::move(data), obj->getHash(), seq);
77 }
78 db.sync();
79}
80
81// One store() per iteration; a fresh Blob copy is handed over each time.
82Workload const kStore{
83 .name = "Store",
84 .setup =
85 [](SetupContext const& ctx) {
86 ctx.rs.present = makePool(1, ctx.poolSize);
87 ctx.rs.avgPayload = averagePayload(ctx.rs.present);
88 },
89 .iterate =
90 [](IterateContext const& ctx) {
91 auto& [rs, db, seq, index, poolSize] = ctx;
92 auto const& obj = rs.present[index % poolSize];
93 Blob data(obj->getData());
94 db.store(obj->getType(), std::move(data), obj->getHash(), seq);
95 },
96 .reportBytes = true,
97 .pinIterations = true,
98};
99
100// One fetchNodeObject() of a stored key (a hit) per iteration.
101Workload const kFetch{
102 .name = "Fetch",
103 .setup =
104 [](SetupContext const& ctx) {
105 ctx.rs.present = makePool(1, ctx.poolSize);
106 ctx.rs.avgPayload = averagePayload(ctx.rs.present);
107 prepopulate(ctx.db, ctx.rs.present);
108 },
109 .iterate =
110 [](IterateContext const& ctx) {
111 auto& [rs, db, seq, index, poolSize] = ctx;
112 auto obj = db.fetchNodeObject(rs.present[index % poolSize]->getHash(), seq);
113 benchmark::DoNotOptimize(obj);
114 },
115 .reportBytes = true,
116};
117
118// One fetchNodeObject() of a never-stored key (a miss) per iteration.
119Workload const kMissing{
120 .name = "Missing",
121 .setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
122 .iterate =
123 [](IterateContext const& ctx) {
124 auto& [rs, db, seq, index, poolSize] = ctx;
125 auto obj = db.fetchNodeObject(rs.missing[index % poolSize], seq);
126 benchmark::DoNotOptimize(obj);
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.
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.db, ctx.rs.present);
141 },
142 .iterate =
143 [](IterateContext const& ctx) {
144 auto& [rs, db, seq, index, poolSize] = ctx;
145 auto const pick = rs.shuffle[index % poolSize];
146 std::shared_ptr<NodeObject> obj;
147 if (index % 5 == 0)
148 {
149 obj = db.fetchNodeObject(rs.missing[pick], seq);
150 }
151 else
152 {
153 obj = db.fetchNodeObject(rs.present[pick]->getHash(), seq);
154 }
155 benchmark::DoNotOptimize(obj);
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.db, ctx.rs.present);
172 },
173 .iterate =
174 [](IterateContext const& ctx) {
175 auto& [rs, db, seq, index, poolSize] = ctx;
176 auto const slot = index % poolSize;
177 auto const pick = rs.shuffle[slot];
178
179 auto historical = db.fetchNodeObject(rs.present[pick]->getHash(), seq);
180 benchmark::DoNotOptimize(historical);
181
182 auto recent = db.fetchNodeObject(rs.recent[pick]->getHash(), seq);
183 benchmark::DoNotOptimize(recent);
184
185 auto const& obj = rs.recent[slot];
186 Blob data(obj->getData());
187 db.store(obj->getType(), std::move(data), obj->getHash(), seq);
188 },
189 .pinIterations = true,
190};
191
192void
193registerWorkload(BackendConfig const& bc, Workload const& w)
194{
195 auto rs = std::make_shared<RunState>();
196 std::string const cfg = bc.config;
197 std::string name{kNamePrefix};
198 name += w.name;
199 name += kNameSeparator;
200 name += bc.name;
201 auto* b = benchmark::RegisterBenchmark(name, [rs, cfg, w](benchmark::State& state) {
202 auto const poolSize = static_cast<std::size_t>(state.range(0));
203 rs->harness = std::make_unique<DatabaseHarness>(cfg, kReadThreads);
204 auto& db = *rs->harness->db;
205 w.setup(SetupContext{.rs = *rs, .db = db, .poolSize = poolSize});
206 auto const seq = db.earliestLedgerSeq();
207
208 std::size_t index = 0;
209 for (auto _ : state)
210 {
211 w.iterate(
212 IterateContext{
213 .rs = *rs, .db = db, .seq = seq, .index = index, .poolSize = poolSize});
214 ++index;
215 }
216 benchmark::ClobberMemory();
217
218 state.SetItemsProcessed(state.iterations());
219 if (w.reportBytes)
220 {
221 state.SetBytesProcessed(static_cast<std::int64_t>(state.iterations() * rs->avgPayload));
222 }
223 rs->harness.reset();
224 });
225
226 b->Arg(kDefaultPoolSize);
227
228 if (w.pinIterations)
229 b->Iterations(kDefaultPoolSize);
230}
231
232[[maybe_unused]] bool const kRegistered = [] {
233 auto const workloads = std::to_array({&kStore, &kFetch, &kMissing, &kMixed, &kWork});
234 for (auto const& bc : backendConfigs())
235 {
236 for (auto const* w : workloads)
237 registerWorkload(bc, *w);
238 }
239 return true;
240}();
241
242} // namespace
243} // namespace xrpl::node_store
Persistency layer for NodeObject.
Definition Database.h:45
T data(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< 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)
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:11