xrpld
Loading...
Searching...
No Matches
NodeStoreBench.h
1#pragma once
2
3#include <xrpl/basics/Blob.h>
4#include <xrpl/basics/ByteUtilities.h>
5#include <xrpl/basics/FileUtilities.h>
6#include <xrpl/basics/base_uint.h>
7#include <xrpl/basics/safe_cast.h>
8#include <xrpl/beast/utility/Journal.h>
9#include <xrpl/beast/xor_shift_engine.h>
10#include <xrpl/config/BasicConfig.h>
11#include <xrpl/nodestore/Backend.h>
12#include <xrpl/nodestore/Database.h>
13#include <xrpl/nodestore/DummyScheduler.h>
14#include <xrpl/nodestore/Manager.h>
15#include <xrpl/nodestore/NodeObject.h>
16#include <xrpl/nodestore/Scheduler.h>
17#include <xrpl/nodestore/Types.h>
18
19#include <boost/algorithm/string/classification.hpp>
20#include <boost/algorithm/string/split.hpp>
21
22#include <algorithm>
23#include <cstddef>
24#include <cstdint>
25#include <cstring>
26#include <memory>
27#include <numeric>
28#include <random>
29#include <ranges>
30#include <string>
31#include <utility>
32#include <vector>
33
34// Shared helpers for the NodeStore benchmarks.
35//
36namespace xrpl::node_store {
37
38// Fill `bytes` of memory at `buffer` with random bits drawn from `g`.
39template <class Generator>
40inline void
41rngcpy(void* buffer, std::size_t bytes, Generator& g)
42{
43 using result_type = typename Generator::result_type;
44 while (bytes > 0)
45 {
46 auto const v = g();
47 auto const chunk = std::min(bytes, sizeof(result_type));
48 std::memcpy(buffer, &v, chunk);
49 buffer = reinterpret_cast<std::uint8_t*>(buffer) + chunk;
50 bytes -= chunk;
51 }
52}
53
65{
66private:
67 static constexpr auto kMinSize = 250;
68 static constexpr auto kMaxSize = 1250;
69
74
75public:
76 explicit Sequence(std::uint8_t prefix)
77 : prefix_(prefix)
78 // uniform distribution over hotLEDGER - hotTRANSACTION_NODE
79 // but exclude hotTRANSACTION = 2 (removed)
80 , dType_({1, 1, 0, 1, 1})
82 {
83 }
84
85 // Returns the n-th key. Used to generate keys that are never stored.
86 // The layout mirrors obj()'s: prefix at byte 0, RNG over the rest, so the
87 // two key spaces stay disjoint by construction (not by coincidence).
90 {
91 gen_.seed(n + 1);
92 uint256 result;
93 auto const data = static_cast<std::uint8_t*>(&*result.begin());
94 *data = prefix_;
95 rngcpy(data + 1, result.size() - 1, gen_);
96 return result;
97 }
98
99 // Returns the n-th complete NodeObject.
102 {
103 gen_.seed(n + 1);
104 uint256 key;
105 auto const data = static_cast<std::uint8_t*>(&*key.begin());
106 *data = prefix_;
107 rngcpy(data + 1, key.size() - 1, gen_);
108 Blob value(dSize_(gen_));
109 rngcpy(&value[0], value.size(), gen_);
111 safeCast<NodeObjectType>(dType_(gen_)), std::move(value), key);
112 }
113
114 // Fills `b` with `size` consecutive NodeObjects starting at index `n`.
115 void
117 {
118 b.clear();
119 b.reserve(size);
120 while ((size--) != 0u)
121 b.push_back(obj(n++));
122 }
123};
124
125// Parse a comma-separated "key=value,key=value" string into a config Section.
126inline Section
128{
129 Section section;
131 boost::split(values, s, boost::algorithm::is_any_of(","));
132 section.append(values);
133 return section;
134}
135
136// Pre-generate `count` distinct objects from key space `prefix`, starting at
137// sequence index `start`.
138inline Batch
140{
141 Sequence seq(prefix);
142 Batch pool;
143 pool.reserve(count);
144 for (auto i = 0uz; i < count; ++i)
145 pool.push_back(seq.obj(start + i));
146 return pool;
147}
148
149// Pre-generate `count` keys disjoint from every `makePool(...)` object, for
150// measuring fetches that miss.
153{
154 Sequence seq(2);
156 keys.reserve(count);
157 for (auto i = 0uz; i < count; ++i)
158 keys.push_back(seq.key(i));
159 return keys;
160}
161
162// Mean payload size across a pool, used for SetBytesProcessed throughput.
163inline std::size_t
165{
166 if (pool.empty())
167 return 0;
168 std::size_t total = 0;
169 for (auto const& obj : pool)
170 total += obj->getData().size();
171 return total / pool.size();
172}
173
174// Store every object and flush, so a following fetch exercises the real read
175// path rather than an in-memory write buffer.
176//
177// We chunk the write at kBatchWriteLimitSize because Types.h documents that as
178// the maximum allowed batch size. NuDB happens to tolerate larger batches
179// today, but the benchmark should not rely on that.
180//
181// sync() is a no-op for both NuDB and RocksDB at the moment (NuDB has a small
182// internal burst buffer that the timed loop will warm up). That is a contract
183// hint, not a guarantee; if either backend ever grows a real flush we get it
184// here for free.
185inline void
186prepopulate(Backend& backend, Batch const& objects)
187{
188 for (std::size_t i = 0; i < objects.size(); i += kBatchWriteLimitSize)
189 {
190 auto const end = std::min(i + kBatchWriteLimitSize, objects.size());
191 backend.storeBatch(Batch(objects.begin() + i, objects.begin() + end));
192 }
193 backend.sync();
194}
195
196// A deterministic permutation of [0, size). Lets the timed loop visit the
197// pre-generated pool in a random-like order with zero RNG cost per iteration -
198// the Timing_test workloads it replaces used uniform_int_distribution per
199// fetch, and a shuffle table reproduces that access pattern without paying for
200// the distribution inside the timed region.
203{
205 std::ranges::iota(v, 0uz);
206 beast::xor_shift_engine gen(seed);
207 std::ranges::shuffle(v, gen);
208 return v;
209}
210
211// Partition a pool into fixed-size batches. Any trailing remainder shorter than
212// `batchSize` is dropped, so every returned batch has exactly `batchSize`.
214sliceFixedBatches(Batch const& pool, std::size_t batchSize)
215{
216 std::vector<Batch> batches;
217 if (batchSize == 0)
218 return batches;
219 batches.reserve(pool.size() / batchSize);
220 for (std::size_t i = 0; i + batchSize <= pool.size(); i += batchSize)
221 batches.emplace_back(pool.begin() + i, pool.begin() + i + batchSize);
222 return batches;
223}
224
229{
234
235 explicit BackendHarness(std::string const& configString)
236 {
237 Section config = parseConfig(configString);
238 // A private, unique path per harness, so concurrent or repeated runs
239 // never share on-disk state.
240 config.set("path", tempDir.path());
241 backend =
243 backend->setDeletePath();
244 backend->open();
245 }
246
248 {
249 if (backend)
250 backend->close();
251 }
252};
253
259{
264
265 DatabaseHarness(std::string const& configString, int readThreads)
266 {
267 Section config = parseConfig(configString);
268 config.set("path", tempDir.path());
270 megabytes(std::size_t{4}), scheduler, readThreads, config, journal);
271 }
272
274 {
275 if (db)
276 db->stop();
277 }
278};
279
280// A NodeStore backend to benchmark, named for the --benchmark_filter CLI flag.
282{
283 char const* name; // short label, e.g. "nudb"
284 char const* config; // parseConfig() string, e.g. "type=nudb"
285};
286
287// The backends every workload is registered against.
288//
289// The in-memory backend is intentionally excluded. It keeps its table in a
290// process-global map keyed by path, with no removal API, so building a fresh
291// backend per run - as a microbenchmark must - would leak the whole dataset on
292// every run. Timing_test, the suite this benchmark replaces, excluded it for
293// the same reason. NuDB and RocksDB are the production backends worth timing.
294//
295// RocksDB is included only when it was compiled in (xrpl.libxrpl carries
296// XRPL_ROCKSDB_AVAILABLE transitively).
297inline std::vector<BackendConfig> const&
299{
300 // Use factory settings for each DB
301 static std::vector<BackendConfig> const kConfigs = {
302 {.name = "nudb", .config = "type=nudb"},
303#if XRPL_ROCKSDB_AVAILABLE
304 {.name = "rocksdb", .config = "type=rocksdb"},
305#endif
306 };
307 return kConfigs;
308}
309
310} // namespace xrpl::node_store
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
static Sink & getNullSink()
Returns a Sink which does nothing.
iterator begin()
Definition base_uint.h:128
static constexpr std::size_t size()
Definition base_uint.h:548
static std::shared_ptr< NodeObject > createObject(NodeObjectType type, Blob &&data, uint256 const &hash)
Create an object from fields.
Holds a collection of configuration values.
Definition BasicConfig.h:29
void set(std::string const &key, std::string const &value)
Set a key/value pair.
void append(std::vector< std::string > const &lines)
Append a set of lines to this section.
RAII temporary directory.
A backend used for the NodeStore.
Definition Backend.h:29
virtual void sync()=0
virtual void storeBatch(Batch const &batch)=0
Store a group of objects.
Simple NodeStore Scheduler that just performs the tasks synchronously.
virtual std::unique_ptr< Backend > makeBackend(Section const &parameters, std::size_t burstSize, Scheduler &scheduler, beast::Journal journal)=0
Create a backend.
virtual std::unique_ptr< Database > makeDatabase(std::size_t burstSize, Scheduler &scheduler, int readThreads, Section const &backendParameters, beast::Journal journal)=0
Construct a NodeStore database.
static Manager & instance()
Returns the instance of the manager singleton.
Deterministic generator of a reproducible sequence of random NodeObjects.
beast::xor_shift_engine gen_
void batch(std::size_t n, Batch &b, std::size_t size)
uint256 key(std::size_t n)
std::uniform_int_distribution< std::uint32_t > dSize_
std::discrete_distribution< std::uint32_t > dType_
static constexpr auto kMaxSize
std::shared_ptr< NodeObject > obj(std::size_t n)
Sequence(std::uint8_t prefix)
static constexpr auto kMinSize
T clear(T... args)
T emplace_back(T... args)
T empty(T... args)
T memcpy(T... args)
T min(T... args)
detail::XorShiftEngine<> xor_shift_engine
XOR-shift Generator.
Section parseConfig(std::string const &s)
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)
void rngcpy(void *buffer, std::size_t bytes, Generator &g)
std::vector< uint256 > makeMissingKeys(std::size_t count)
static constexpr auto kBatchWriteLimitSize
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)
constexpr Dest safeCast(Src s) noexcept
Definition safe_cast.h:21
constexpr auto megabytes(T value) noexcept
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:11
BaseUInt< 256 > uint256
Definition base_uint.h:580
T push_back(T... args)
T reserve(T... args)
T shuffle(T... args)
T size(T... args)
std::unique_ptr< Backend > backend
TempDir tempDir
Declared first so it is destroyed last.
BackendHarness(std::string const &configString)
std::unique_ptr< Database > db
DatabaseHarness(std::string const &configString, int readThreads)