Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ words:
- xchain
- ximinez
- XMACRO
- xored
- xrpkuwait
- xrpl
- xrpld
Expand Down
2 changes: 2 additions & 0 deletions cmake/XrplAddBenchmark.cmake
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
include_guard()

include(isolate_headers)

# Define a benchmark executable for the module `name`.
Expand Down
1 change: 1 addition & 0 deletions cmake/XrplCov.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ setup_target_for_coverage_gcovr(
EXCLUDE
"src/test"
"src/tests"
"src/benchmarks"
"include/xrpl/beast/test"
"include/xrpl/beast/unit_test"
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
Expand Down
14 changes: 14 additions & 0 deletions include/xrpl/basics/Buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/instrumentation.h>

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <memory>
Expand Down Expand Up @@ -156,6 +157,19 @@ class Buffer
}
/** @} */

/**
* Set every byte in the buffer to the given value.
*
* The size is unchanged, and this is a no-op on an empty buffer.
*
* @param value the byte to write to every position.
*/
void
fill(std::uint8_t value) noexcept
{
std::fill_n(p_.get(), size_, value);
}

/**
* Reset the buffer.
* All memory is deallocated. The resulting size is 0.
Expand Down
50 changes: 29 additions & 21 deletions src/benchmarks/libxrpl/nodestore/Backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,34 @@
namespace xrpl::node_store {
namespace {

constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000};
constexpr int kThreadCounts[] = {1, 4, 8};
constexpr auto kPoolSizes = std::to_array<std::size_t>({1000, 10000, 100000});
constexpr auto kThreadCounts = std::to_array<std::size_t>({1, 4, 8});
constexpr std::size_t kBatchSize = 256;
constexpr std::size_t kMissRatio = 5;

constexpr std::string_view kNamePrefix = "BM_Backend_";
constexpr std::string_view kNameSeparator = "/";

struct RunState
{
std::unique_ptr<BackendHarness> harness;
Batch present; // prefix-1 objects, eligible to be stored
Batch recent; // prefix-1 objects in the "future" key space
std::vector<uint256> missing; // prefix-2 keys that are never stored
std::vector<std::size_t> shuffle; // [0, poolSize) permutation for random-like access
std::size_t avgPayload = 0; // mean getData().size() over `present`
std::unique_ptr<BackendHarness> harness; ///< backend under test, rebuilt per run
Batch present; ///< prefix-1 objects, eligible to be stored
Batch recent; ///< prefix-1 objects in the "future" key space
std::vector<uint256> missing; ///< prefix-2 keys that are never stored
std::vector<std::size_t> shuffle; ///< [0, poolSize) permutation for random-like access
std::size_t avgPayload = 0; ///< mean getData().size() over `present`

void
release()
{
harness.reset();
// swap-with-empty rather than assignment: only swapping is guaranteed to
// hand the capacity back, and these pools are large.
Batch{}.swap(present);
Batch{}.swap(recent);
std::vector<uint256>{}.swap(missing);
std::vector<std::size_t>{}.swap(shuffle);
avgPayload = 0;
}
};

Expand Down Expand Up @@ -85,7 +89,7 @@ Workload const kInsert{
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const& [rs, backend, index, poolSize] = ctx;
backend.store(rs.present[index % poolSize]);
},
.reportBytes = true,
Expand All @@ -104,7 +108,7 @@ Workload const kFetch{
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
backend.fetch(rs.present[index % poolSize]->getHash(), &result);
benchmark::DoNotOptimize(result);
Expand All @@ -118,7 +122,7 @@ Workload const kMissing{
.setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); },
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
backend.fetch(rs.missing[index % poolSize], &result);
benchmark::DoNotOptimize(result);
Expand All @@ -139,10 +143,10 @@ Workload const kMixed{
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const& [rs, backend, index, poolSize] = ctx;
std::shared_ptr<NodeObject> result;
auto const pick = rs.shuffle[index % poolSize];
if (index % 5 == 0)
if (index % kMissRatio == 0)
{
backend.fetch(rs.missing[pick], &result);
}
Expand Down Expand Up @@ -170,7 +174,7 @@ Workload const kWork{
},
.iterate =
[](IterateContext const& ctx) {
auto& [rs, backend, index, poolSize] = ctx;
auto const& [rs, backend, index, poolSize] = ctx;
auto const slot = index % poolSize;
auto const pick = rs.shuffle[slot];

Expand Down Expand Up @@ -238,9 +242,13 @@ registerWorkload(BackendConfig const& bc, Workload const& w)
if (!w.pinToPool)
{
auto rs = std::make_shared<RunState>();
auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs));
b->RangeMultiplier(10)->Range(kPoolSizes[0], kPoolSizes[std::size(kPoolSizes) - 1]);
b->Threads(1)->Threads(4)->Threads(8)->UseRealTime();
benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
->RangeMultiplier(10)
->Range(kPoolSizes.front(), kPoolSizes.back())
->Threads(1)
->Threads(4)
->Threads(8)
->UseRealTime();

return;
}
Expand All @@ -249,14 +257,14 @@ registerWorkload(BackendConfig const& bc, Workload const& w)
{
for (auto const threads : kThreadCounts)
{
if (poolSize % static_cast<std::size_t>(threads) != 0)
if (poolSize % threads != 0)
continue;

auto rs = std::make_shared<RunState>();
benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
->Arg(poolSize)
->Iterations(poolSize / static_cast<std::size_t>(threads))
->Threads(threads)
->Iterations(poolSize / threads)
->Threads(static_cast<int>(threads))
->UseRealTime();
}
}
Expand Down Expand Up @@ -289,7 +297,7 @@ registerStoreBatch(BackendConfig const& bc)
rs->harness = std::make_unique<BackendHarness>(cfg);
rs->present = makePool(1, poolSize);
rs->avgPayload = averagePayload(rs->present);
std::vector<Batch> const batches = sliceBatches(rs->present, kBatchSize);
std::vector<Batch> const batches = slicePreciseBatches(rs->present, kBatchSize);
if (batches.empty())
{
state.SkipWithError("pool smaller than one batch");
Expand Down
36 changes: 14 additions & 22 deletions src/benchmarks/libxrpl/nodestore/NodeStoreBench.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <memory>
#include <numeric>
#include <random>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
Expand All @@ -40,18 +41,13 @@ inline void
rngcpy(void* buffer, std::size_t bytes, Generator& g)
{
using result_type = typename Generator::result_type;
while (bytes >= sizeof(result_type))
while (bytes > 0)
{
auto const v = g();
std::memcpy(buffer, &v, sizeof(v));
buffer = reinterpret_cast<std::uint8_t*>(buffer) + sizeof(v);
bytes -= sizeof(v);
}

if (bytes > 0)
{
auto const v = g();
std::memcpy(buffer, &v, bytes);
auto const chunk = std::min(bytes, sizeof(result_type));
std::memcpy(buffer, &v, chunk);
buffer = reinterpret_cast<std::uint8_t*>(buffer) + chunk;
bytes -= chunk;
}
}

Expand Down Expand Up @@ -145,7 +141,7 @@ makePool(std::uint8_t prefix, std::size_t count, std::size_t start = 0)
Sequence seq(prefix);
Batch pool;
pool.reserve(count);
for (std::size_t i = 0; i < count; ++i)
for (auto const i : std::views::iota(0uz, count))
pool.push_back(seq.obj(start + i));
return pool;
}
Expand All @@ -158,7 +154,7 @@ makeMissingKeys(std::size_t count)
Sequence seq(2);
std::vector<uint256> keys;
keys.reserve(count);
for (std::size_t i = 0; i < count; ++i)
for (auto const i : std::views::iota(0uz, count))
keys.push_back(seq.key(i));
return keys;
}
Expand Down Expand Up @@ -206,16 +202,16 @@ inline std::vector<std::size_t>
makeShuffle(std::size_t size, std::uint64_t seed)
{
std::vector<std::size_t> v(size);
std::iota(v.begin(), v.end(), std::size_t{0});
std::ranges::iota(v, 0uz);
beast::xor_shift_engine gen(seed);
std::shuffle(v.begin(), v.end(), gen);
std::ranges::shuffle(v, gen);
return v;
}

// Partition a pool into fixed-size batches. Any trailing remainder shorter than
// `batchSize` is dropped, so every returned batch has exactly `batchSize`.
inline std::vector<Batch>
sliceBatches(Batch const& pool, std::size_t batchSize)
slicePreciseBatches(Batch const& pool, std::size_t batchSize)
{
std::vector<Batch> batches;
if (batchSize == 0)
Expand All @@ -228,13 +224,10 @@ sliceBatches(Batch const& pool, std::size_t batchSize)

/**
* @brief RAII owner of a NodeStore Backend opened on a private temporary directory.
*
* Member declaration order matters: `tempDir` is declared first so it is
* destroyed last, after the backend has closed and released its files.
*/
struct BackendHarness
{
beast::TempDir tempDir;
beast::TempDir tempDir; ///< Declared first so it is destroyed last
DummyScheduler scheduler;
beast::Journal journal{beast::Journal::getNullSink()};
std::unique_ptr<Backend> backend;
Expand Down Expand Up @@ -304,12 +297,11 @@ struct BackendConfig
inline std::vector<BackendConfig> const&
backendConfigs()
{
// Use factory settings for each DB
static std::vector<BackendConfig> const kConfigs = {
{.name = "nudb", .config = "type=nudb"},
#if XRPL_ROCKSDB_AVAILABLE
{.name = "rocksdb",
.config = "type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256,"
"file_size_mb=8,file_size_mult=2"},
{.name = "rocksdb", .config = "type=rocksdb"},
#endif
};
return kConfigs;
Expand Down
Loading
Loading