From 66be91d5b376275d5726fdb2f1e2c9dd640f3fe3 Mon Sep 17 00:00:00 2001 From: Andrzej Budzanowski Date: Thu, 16 Jul 2026 17:44:43 +0200 Subject: [PATCH] test: Add google benchmark dependency and migrate `nodestore` timing test as a benchmark (#7317) Co-authored-by: Marek Foss Co-authored-by: Alex Kremer --- .gersemi/definitions.cmake | 3 + .../scripts/levelization/results/ordering.txt | 3 + .../workflows/reusable-build-test-config.yml | 17 + CMakeLists.txt | 8 + CONTRIBUTING.md | 1 + cmake/XrplAddBenchmark.cmake | 36 + cmake/XrplSettings.cmake | 2 + conan.lock | 1 + conanfile.py | 5 + src/benchmarks/libxrpl/CMakeLists.txt | 22 + src/benchmarks/libxrpl/nodestore/Backend.cpp | 329 ++++++++ src/benchmarks/libxrpl/nodestore/Database.cpp | 243 ++++++ .../libxrpl/nodestore/NodeStoreBench.h | 318 ++++++++ src/test/nodestore/Timing_test.cpp | 729 ------------------ 14 files changed, 988 insertions(+), 729 deletions(-) create mode 100644 cmake/XrplAddBenchmark.cmake create mode 100644 src/benchmarks/libxrpl/CMakeLists.txt create mode 100644 src/benchmarks/libxrpl/nodestore/Backend.cpp create mode 100644 src/benchmarks/libxrpl/nodestore/Database.cpp create mode 100644 src/benchmarks/libxrpl/nodestore/NodeStoreBench.h delete mode 100644 src/test/nodestore/Timing_test.cpp diff --git a/.gersemi/definitions.cmake b/.gersemi/definitions.cmake index aa63076c8b..0932a72463 100644 --- a/.gersemi/definitions.cmake +++ b/.gersemi/definitions.cmake @@ -11,6 +11,9 @@ endfunction() function(create_symbolic_link target link) endfunction() +function(xrpl_add_benchmark name) +endfunction() + macro(exclude_from_default target_) endmacro() diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index b31e6ae961..3c9c514516 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -1,3 +1,6 @@ +benchmarks.libxrpl > xrpl.basics +benchmarks.libxrpl > xrpl.config +benchmarks.libxrpl > xrpl.nodestore libxrpl.basics > xrpl.basics libxrpl.conditions > xrpl.basics libxrpl.conditions > xrpl.conditions diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 975b788c0d..d4135207fe 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -324,6 +324,23 @@ jobs: LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log + # Smoke-run every benchmark module with a single repetition to confirm the + # benchmarks still build and execute. This is a correctness check, not a + # performance measurement, so it is skipped for instrumented builds + # (sanitizers/coverage/voidstar), where it would be slow and meaningless, + # and on Windows, where the `install` target does not build them. + - name: Run the benchmarks + if: ${{ !inputs.build_only && runner.os != 'Windows' && env.SANITIZERS_ENABLED == 'false' && env.COVERAGE_ENABLED != 'true' && env.VOIDSTAR_ENABLED != 'true' }} + working-directory: ${{ env.BUILD_DIR }} + run: | + rc=0 + while IFS= read -r bench; do + echo "::group::${bench}" + "./${bench}" --benchmark_repetitions=1 || rc=1 + echo "::endgroup::" + done < <(find src/benchmarks -type f -perm -u+x -name 'xrpl.bench.*') + exit "${rc}" + - name: Show test failure summary if: ${{ failure() && !inputs.build_only }} env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e8befcc8f..f2e8fb3ae5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,6 +131,10 @@ else() endif() target_link_libraries(xrpl_libs INTERFACE ${nudb}) +if(benchmark) + find_package(benchmark REQUIRED) +endif() + if(coverage) include(XrplCov) endif() @@ -145,3 +149,7 @@ if(tests) include(CTest) add_subdirectory(src/tests/libxrpl) endif() + +if(benchmark) + add_subdirectory(src/benchmarks/libxrpl) +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9929b2eb39..7632741e35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,6 +83,7 @@ If you create new source files, they must be organized as follows: `src/libxrpl`. - All other non-test files must go under `src/xrpld`. - All test source files must go under `src/test`. +- All benchmark source files must go under `src/benchmarks`. The source must be formatted according to the style guide below. The easiest way to satisfy this is to install the [`pre-commit`](#pre-commit-hooks) hooks, diff --git a/cmake/XrplAddBenchmark.cmake b/cmake/XrplAddBenchmark.cmake new file mode 100644 index 0000000000..1dd875dd61 --- /dev/null +++ b/cmake/XrplAddBenchmark.cmake @@ -0,0 +1,36 @@ +include(isolate_headers) + +# Define a benchmark executable for the module `name`. +# +# This follows the same general pattern as other build helpers in this repo +# (e.g. `add_module`): create a target and isolate headers, but here the target +# is a benchmark executable and no `add_test(...)` is registered. +# +# `isolate_headers` exposes only `${CMAKE_CURRENT_SOURCE_DIR}/${name}` on the +# include path, rooted at `src`, so a benchmark's own headers are reached as +# `` and nothing else in the tree leaks in. +function(xrpl_add_benchmark name) + set(target ${PROJECT_NAME}.bench.${name}) + + file( + GLOB_RECURSE sources + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/${name}/*.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/${name}.cpp" + ) + add_executable(${target} ${ARGN} ${sources}) + + # Benchmark sources register cases through Google Benchmark's static + # registrars (anonymous-namespace lambdas). Merging several such files into + # one unity translation unit collides those internal-linkage entities, so + # keep benchmarks out of the unity build - mirroring xrpl.libpb in + # XrplCore.cmake. Each file compiles fine on its own. + set_target_properties(${target} PROPERTIES UNITY_BUILD OFF) + + isolate_headers( + ${target} + "${CMAKE_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}/${name}" + PRIVATE + ) +endfunction() diff --git a/cmake/XrplSettings.cmake b/cmake/XrplSettings.cmake index 757a596096..be9bf1fda2 100644 --- a/cmake/XrplSettings.cmake +++ b/cmake/XrplSettings.cmake @@ -30,6 +30,8 @@ if(tests) endif() endif() +option(benchmark "Build benchmarks" ON) + # Enabled by default so every header is compiled on its own as the main file of # its own compile_commands.json entry - this is what lets clang-tidy (and clangd # and IDEs) analyse a header's own includes directly. The per-header objects are diff --git a/conan.lock b/conan.lock index 9dfbb86960..c6a4070c77 100644 --- a/conan.lock +++ b/conan.lock @@ -25,6 +25,7 @@ "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654", "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732", "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605", + "benchmark/1.9.5#b885dc73ad67b40a55d45684d1c88ad1%1782736613.864841", "abseil/20250127.0#9ef01c1451a8340f9022e46238c0fbb6%1783945159.651047" ], "build_requires": [ diff --git a/conanfile.py b/conanfile.py index f0b10cf34b..f883761f0e 100644 --- a/conanfile.py +++ b/conanfile.py @@ -15,6 +15,7 @@ class Xrpl(ConanFile): settings = "os", "compiler", "build_type", "arch" options = { "assertions": [True, False], + "benchmark": [True, False], "coverage": [True, False], "fPIC": [True, False], "jemalloc": [True, False], @@ -46,6 +47,7 @@ class Xrpl(ConanFile): default_options = { "assertions": False, + "benchmark": True, "coverage": False, "fPIC": True, "jemalloc": False, @@ -129,6 +131,8 @@ class Xrpl(ConanFile): self.options["boost"].without_cobalt = True def requirements(self): + if self.options.benchmark: + self.requires("benchmark/1.9.5") self.requires("boost/1.91.0", force=True, transitive_headers=True) self.requires("date/3.0.4", transitive_headers=True) if self.options.jemalloc: @@ -162,6 +166,7 @@ class Xrpl(ConanFile): def generate(self): tc = CMakeToolchain(self) tc.variables["tests"] = self.options.tests + tc.variables["benchmark"] = self.options.benchmark tc.variables["assert"] = self.options.assertions tc.variables["coverage"] = self.options.coverage tc.variables["jemalloc"] = self.options.jemalloc diff --git a/src/benchmarks/libxrpl/CMakeLists.txt b/src/benchmarks/libxrpl/CMakeLists.txt new file mode 100644 index 0000000000..ac751a0413 --- /dev/null +++ b/src/benchmarks/libxrpl/CMakeLists.txt @@ -0,0 +1,22 @@ +include(XrplAddBenchmark) + +# Benchmark requirements. +find_package(benchmark REQUIRED) + +# Custom target for all benchmarks defined in this file. +add_custom_target(xrpl.benchmarks) + +# Common library dependencies for every benchmark module. `benchmark_main` +# supplies a `main()` that parses the standard Google Benchmark CLI flags +# (`--benchmark_filter`, `--benchmark_format`, ...), so no per-module main.cpp +# is needed. +add_library(xrpl.imports.bench INTERFACE) +target_link_libraries( + xrpl.imports.bench + INTERFACE benchmark::benchmark_main xrpl.libxrpl +) + +# One benchmark executable for each module. +xrpl_add_benchmark(nodestore) +target_link_libraries(xrpl.bench.nodestore PRIVATE xrpl.imports.bench) +add_dependencies(xrpl.benchmarks xrpl.bench.nodestore) diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp new file mode 100644 index 0000000000..7db0185053 --- /dev/null +++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp @@ -0,0 +1,329 @@ +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::NodeStore { +namespace { + +constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000}; +constexpr int kThreadCounts[] = {1, 4, 8}; +constexpr std::size_t kBatchSize = 256; + +constexpr std::string_view kNamePrefix = "BM_Backend_"; +constexpr std::string_view kNameSeparator = "/"; + +struct RunState +{ + std::unique_ptr harness; + Batch present; // prefix-1 objects, eligible to be stored + Batch recent; // prefix-1 objects in the "future" key space + std::vector missing; // prefix-2 keys that are never stored + std::vector shuffle; // [0, poolSize) permutation for random-like access + std::size_t avgPayload = 0; // mean getData().size() over `present` + + void + release() + { + harness.reset(); + Batch{}.swap(present); + Batch{}.swap(recent); + std::vector{}.swap(missing); + std::vector{}.swap(shuffle); + } +}; + +struct SetupContext +{ + RunState& rs; + Backend& backend; + std::size_t poolSize; +}; + +struct IterateContext +{ + RunState& rs; + Backend& backend; + std::size_t index; + std::size_t poolSize; +}; + +struct Workload +{ + std::string_view name; + std::function setup; + std::function iterate; + bool reportBytes = false; // SetBytesProcessed from rs.avgPayload + bool clobber = true; // ClobberMemory after the loop (false for pure stores) + bool pinToPool = false; // pin iterations to one pool sweep instead of autotuning +}; + +// One store() per iteration. Iterations are pinned to one pool sweep (per +// thread) so the index never wraps past the pool - otherwise NuDB::doInsert +// swallows key_exists and the workload degenerates into duplicate-detection +// no-ops. +Workload const kInsert{ + .name = "Insert", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.avgPayload = averagePayload(ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, backend, index, poolSize] = ctx; + backend.store(rs.present[index % poolSize]); + }, + .reportBytes = true, + .clobber = false, + .pinToPool = true, +}; + +// One fetch() of a present key (a hit) per iteration. +Workload const kFetch{ + .name = "Fetch", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.avgPayload = averagePayload(ctx.rs.present); + prepopulate(ctx.backend, ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, backend, index, poolSize] = ctx; + std::shared_ptr result; + backend.fetch(rs.present[index % poolSize]->getHash(), &result); + benchmark::DoNotOptimize(result); + }, + .reportBytes = true, +}; + +// One fetch() of a never-stored key (a miss); the backend is left empty. +Workload const kMissing{ + .name = "Missing", + .setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, backend, index, poolSize] = ctx; + std::shared_ptr result; + backend.fetch(rs.missing[index % poolSize], &result); + benchmark::DoNotOptimize(result); + }, +}; + +// 80% hits / 20% misses. The fetch index comes from a shuffle table so access +// is random-like without per-iteration RNG cost; sequential `index % poolSize` +// would be artificially cache-friendly to RocksDB's block cache. +Workload const kMixed{ + .name = "Mixed", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.missing = makeMissingKeys(ctx.poolSize); + ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1); + prepopulate(ctx.backend, ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, backend, index, poolSize] = ctx; + std::shared_ptr result; + auto const pick = rs.shuffle[index % poolSize]; + if (index % 5 == 0) + { + backend.fetch(rs.missing[pick], &result); + } + else + { + backend.fetch(rs.present[pick]->getHash(), &result); + } + benchmark::DoNotOptimize(result); + }, +}; + +// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The +// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item +// it's about to store this iteration - which would give an all-miss-then-hit +// step instead of a smooth ramp. The store walks sequentially so each recent +// object is stored once. +Workload const kWork{ + .name = "Work", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize); + ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2); + prepopulate(ctx.backend, ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, backend, index, poolSize] = ctx; + auto const slot = index % poolSize; + auto const pick = rs.shuffle[slot]; + + std::shared_ptr historical; + backend.fetch(rs.present[pick]->getHash(), &historical); + benchmark::DoNotOptimize(historical); + + std::shared_ptr recent; + backend.fetch(rs.recent[pick]->getHash(), &recent); + benchmark::DoNotOptimize(recent); + + backend.store(rs.recent[slot]); + }, + .clobber = true, + .pinToPool = true, +}; + +auto +makeRunner(Workload w, std::string cfg, std::shared_ptr rs) +{ + return [w = std::move(w), cfg = std::move(cfg), rs = std::move(rs)](benchmark::State& state) { + auto const poolSize = static_cast(state.range(0)); + if (state.thread_index() == 0) + { + rs->harness = std::make_unique(cfg); + w.setup( + SetupContext{.rs = *rs, .backend = *rs->harness->backend, .poolSize = poolSize}); + } + + std::size_t index = state.thread_index(); + for (auto _ : state) + { + w.iterate( + IterateContext{ + .rs = *rs, + .backend = *rs->harness->backend, + .index = index, + .poolSize = poolSize}); + index += state.threads(); + } + + if (w.clobber) + benchmark::ClobberMemory(); + + state.SetItemsProcessed(state.iterations()); + if (w.reportBytes) + state.SetBytesProcessed(static_cast(state.iterations() * rs->avgPayload)); + + if (state.thread_index() == 0) + rs->release(); + }; +} + +// Register workload `w` against backend `bc`, choosing the registration shape +// from `w.pinToPool`. +void +registerWorkload(BackendConfig const& bc, Workload const& w) +{ + std::string const cfg = bc.config; + std::string name{kNamePrefix}; + name += w.name; + name += kNameSeparator; + name += bc.name; + + if (!w.pinToPool) + { + auto rs = std::make_shared(); + 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(); + + return; + } + + for (auto const poolSize : kPoolSizes) + { + for (auto const threads : kThreadCounts) + { + if (poolSize % static_cast(threads) != 0) + continue; + + auto rs = std::make_shared(); + benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)) + ->Arg(poolSize) + ->Iterations(poolSize / static_cast(threads)) + ->Threads(threads) + ->UseRealTime(); + } + } +} + +// One storeBatch() of kBatchSize objects per iteration. Single-threaded: +// Backend::storeBatch must not run concurrently with itself or store(). +// Iterations are pinned to the batch count so the index never wraps into +// key_exists no-ops. Kept separate from Workload: batch slicing and the +// per-batch item/byte accounting don't fit the thread-axis mold. +void +registerStoreBatch(BackendConfig const& bc) +{ + std::string const cfg = bc.config; + std::string name{kNamePrefix}; + name += "StoreBatch"; + name += kNameSeparator; + name += bc.name; + for (auto const poolSize : kPoolSizes) + { + auto const numBatches = poolSize / kBatchSize; + if (numBatches == 0) + continue; + + auto rs = std::make_shared(); + benchmark::RegisterBenchmark( + name, + [rs, cfg](benchmark::State& state) { + auto const poolSize = static_cast(state.range(0)); + rs->harness = std::make_unique(cfg); + rs->present = makePool(1, poolSize); + rs->avgPayload = averagePayload(rs->present); + std::vector const batches = sliceBatches(rs->present, kBatchSize); + if (batches.empty()) + { + state.SkipWithError("pool smaller than one batch"); + return; + } + + std::size_t index = 0; + for (auto _ : state) + { + rs->harness->backend->storeBatch(batches[index % batches.size()]); + ++index; + } + + state.SetItemsProcessed(static_cast(state.iterations() * kBatchSize)); + state.SetBytesProcessed( + static_cast(state.iterations() * kBatchSize * rs->avgPayload)); + rs->release(); + }) + ->Arg(poolSize) + ->Iterations(numBatches); + } +} + +[[maybe_unused]] bool const kRegistered = [] { + auto const workloads = std::to_array({&kInsert, &kFetch, &kMissing, &kMixed, &kWork}); + for (auto const& bc : backendConfigs()) + { + for (auto const* w : workloads) + registerWorkload(bc, *w); + + registerStoreBatch(bc); + } + return true; +}(); + +} // namespace +} // namespace xrpl::NodeStore diff --git a/src/benchmarks/libxrpl/nodestore/Database.cpp b/src/benchmarks/libxrpl/nodestore/Database.cpp new file mode 100644 index 0000000000..2303075ab9 --- /dev/null +++ b/src/benchmarks/libxrpl/nodestore/Database.cpp @@ -0,0 +1,243 @@ +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::NodeStore { +namespace { + +// Number of distinct objects pre-generated per run. +constexpr std::size_t kDefaultPoolSize = 100000; + +// Async read threads the Database spawns. Unused by the synchronous fetch path +// these benchmarks take; kept fixed so runs are comparable. +constexpr int kReadThreads = 4; + +constexpr std::string_view kNamePrefix = "BM_Database_"; +constexpr std::string_view kNameSeparator = "/"; + +struct RunState +{ + std::unique_ptr harness; + Batch present; // prefix-1 objects, eligible to be stored + Batch recent; // prefix-1 objects in the "future" key space + std::vector missing; // prefix-2 keys that are never stored + std::vector shuffle; // [0, poolSize) permutation for random-like access + std::size_t avgPayload = 0; // mean getData().size() over `present` +}; + +struct SetupContext +{ + RunState& rs; + Database& db; + std::size_t poolSize; +}; + +struct IterateContext +{ + RunState& rs; + Database& db; + std::uint32_t seq; + std::size_t index; + std::size_t poolSize; +}; + +struct Workload +{ + std::string_view name; + std::function setup; + std::function iterate; + bool reportBytes = false; + bool pinIterations = false; +}; + +void +prepopulate(Database& db, Batch const& objects) +{ + auto const seq = db.earliestLedgerSeq(); + for (auto const& obj : objects) + { + Blob data(obj->getData()); + db.store(obj->getType(), std::move(data), obj->getHash(), seq); + } + db.sync(); +} + +// One store() per iteration; a fresh Blob copy is handed over each time. +Workload const kStore{ + .name = "Store", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.avgPayload = averagePayload(ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, db, seq, index, poolSize] = ctx; + auto const& obj = rs.present[index % poolSize]; + Blob data(obj->getData()); + db.store(obj->getType(), std::move(data), obj->getHash(), seq); + }, + .reportBytes = true, + .pinIterations = true, +}; + +// One fetchNodeObject() of a stored key (a hit) per iteration. +Workload const kFetch{ + .name = "Fetch", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.avgPayload = averagePayload(ctx.rs.present); + prepopulate(ctx.db, ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, db, seq, index, poolSize] = ctx; + auto obj = db.fetchNodeObject(rs.present[index % poolSize]->getHash(), seq); + benchmark::DoNotOptimize(obj); + }, + .reportBytes = true, +}; + +// One fetchNodeObject() of a never-stored key (a miss) per iteration. +Workload const kMissing{ + .name = "Missing", + .setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, db, seq, index, poolSize] = ctx; + auto obj = db.fetchNodeObject(rs.missing[index % poolSize], seq); + benchmark::DoNotOptimize(obj); + }, +}; + +// 80% hits / 20% misses. The fetch index comes from a shuffle table so access +// is random-like without per-iteration RNG cost; sequential `index % poolSize` +// would be artificially cache-friendly. +Workload const kMixed{ + .name = "Mixed", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.missing = makeMissingKeys(ctx.poolSize); + ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/1); + prepopulate(ctx.db, ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, db, seq, index, poolSize] = ctx; + auto const pick = rs.shuffle[index % poolSize]; + std::shared_ptr obj; + if (index % 5 == 0) + { + obj = db.fetchNodeObject(rs.missing[pick], seq); + } + else + { + obj = db.fetchNodeObject(rs.present[pick]->getHash(), seq); + } + benchmark::DoNotOptimize(obj); + }, +}; + +// An xrpld-like cycle: a hit, a maybe-miss recent fetch, and a store. The +// recent fetch uses the shuffle table (not `slot`) so it doesn't fetch the item +// it's about to store this iteration - which would give an all-miss-then-hit +// step instead of a smooth ramp. The store walks sequentially so each recent +// object is stored once. +Workload const kWork{ + .name = "Work", + .setup = + [](SetupContext const& ctx) { + ctx.rs.present = makePool(1, ctx.poolSize); + ctx.rs.recent = makePool(1, ctx.poolSize, ctx.poolSize); + ctx.rs.shuffle = makeShuffle(ctx.poolSize, /*seed=*/2); + prepopulate(ctx.db, ctx.rs.present); + }, + .iterate = + [](IterateContext const& ctx) { + auto& [rs, db, seq, index, poolSize] = ctx; + auto const slot = index % poolSize; + auto const pick = rs.shuffle[slot]; + + auto historical = db.fetchNodeObject(rs.present[pick]->getHash(), seq); + benchmark::DoNotOptimize(historical); + + auto recent = db.fetchNodeObject(rs.recent[pick]->getHash(), seq); + benchmark::DoNotOptimize(recent); + + auto const& obj = rs.recent[slot]; + Blob data(obj->getData()); + db.store(obj->getType(), std::move(data), obj->getHash(), seq); + }, + .pinIterations = true, +}; + +void +registerWorkload(BackendConfig const& bc, Workload const& w) +{ + auto rs = std::make_shared(); + std::string const cfg = bc.config; + std::string name{kNamePrefix}; + name += w.name; + name += kNameSeparator; + name += bc.name; + auto* b = benchmark::RegisterBenchmark(name, [rs, cfg, w](benchmark::State& state) { + auto const poolSize = static_cast(state.range(0)); + rs->harness = std::make_unique(cfg, kReadThreads); + auto& db = *rs->harness->db; + w.setup(SetupContext{.rs = *rs, .db = db, .poolSize = poolSize}); + auto const seq = db.earliestLedgerSeq(); + + std::size_t index = 0; + for (auto _ : state) + { + w.iterate( + IterateContext{ + .rs = *rs, .db = db, .seq = seq, .index = index, .poolSize = poolSize}); + ++index; + } + benchmark::ClobberMemory(); + + state.SetItemsProcessed(state.iterations()); + if (w.reportBytes) + { + state.SetBytesProcessed(static_cast(state.iterations() * rs->avgPayload)); + } + rs->harness.reset(); + }); + + b->Arg(kDefaultPoolSize); + + if (w.pinIterations) + b->Iterations(kDefaultPoolSize); +} + +[[maybe_unused]] bool const kRegistered = [] { + auto const workloads = std::to_array({&kStore, &kFetch, &kMissing, &kMixed, &kWork}); + for (auto const& bc : backendConfigs()) + { + for (auto const* w : workloads) + registerWorkload(bc, *w); + } + return true; +}(); + +} // namespace +} // namespace xrpl::NodeStore diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h new file mode 100644 index 0000000000..fe6c2a350e --- /dev/null +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -0,0 +1,318 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Shared helpers for the NodeStore benchmarks. +// +namespace xrpl::NodeStore { + +// Fill `bytes` of memory at `buffer` with random bits drawn from `g`. +template +inline void +rngcpy(void* buffer, std::size_t bytes, Generator& g) +{ + using result_type = typename Generator::result_type; + while (bytes >= sizeof(result_type)) + { + auto const v = g(); + std::memcpy(buffer, &v, sizeof(v)); + buffer = reinterpret_cast(buffer) + sizeof(v); + bytes -= sizeof(v); + } + + if (bytes > 0) + { + auto const v = g(); + std::memcpy(buffer, &v, bytes); + } +} + +/** + * @brief Deterministic generator of a reproducible sequence of random NodeObjects. + * + * Indexing is stable: `obj(n)` and `key(n)` always return the same value for a + * given `n`, regardless of call order, because the engine is reseeded from `n` + * on every call. + * + * Using different prefixes guarantees the two key spaces are disjoint for the fetch-miss + * workloads. + */ +class Sequence +{ +private: + static constexpr auto kMinSize = 250; + static constexpr auto kMaxSize = 1250; + + beast::xor_shift_engine gen_; + std::uint8_t prefix_; + std::discrete_distribution dType_; + std::uniform_int_distribution dSize_; + +public: + explicit Sequence(std::uint8_t prefix) + : prefix_(prefix) + // uniform distribution over hotLEDGER - hotTRANSACTION_NODE + // but exclude hotTRANSACTION = 2 (removed) + , dType_({1, 1, 0, 1, 1}) + , dSize_(kMinSize, kMaxSize) + { + } + + // Returns the n-th key. Used to generate keys that are never stored. + // The layout mirrors obj()'s: prefix at byte 0, RNG over the rest, so the + // two key spaces stay disjoint by construction (not by coincidence). + uint256 + key(std::size_t n) + { + gen_.seed(n + 1); + uint256 result; + auto const data = static_cast(&*result.begin()); + *data = prefix_; + rngcpy(data + 1, result.size() - 1, gen_); + return result; + } + + // Returns the n-th complete NodeObject. + std::shared_ptr + obj(std::size_t n) + { + gen_.seed(n + 1); + uint256 key; + auto const data = static_cast(&*key.begin()); + *data = prefix_; + rngcpy(data + 1, key.size() - 1, gen_); + Blob value(dSize_(gen_)); + rngcpy(&value[0], value.size(), gen_); + return NodeObject::createObject( + safeCast(dType_(gen_)), std::move(value), key); + } + + // Fills `b` with `size` consecutive NodeObjects starting at index `n`. + void + batch(std::size_t n, Batch& b, std::size_t size) + { + b.clear(); + b.reserve(size); + while ((size--) != 0u) + b.push_back(obj(n++)); + } +}; + +// Parse a comma-separated "key=value,key=value" string into a config Section. +inline Section +parseConfig(std::string const& s) +{ + Section section; + std::vector values; + boost::split(values, s, boost::algorithm::is_any_of(",")); + section.append(values); + return section; +} + +// Pre-generate `count` distinct objects from key space `prefix`, starting at +// sequence index `start`. +inline Batch +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) + pool.push_back(seq.obj(start + i)); + return pool; +} + +// Pre-generate `count` keys disjoint from every `makePool(...)` object, for +// measuring fetches that miss. +inline std::vector +makeMissingKeys(std::size_t count) +{ + Sequence seq(2); + std::vector keys; + keys.reserve(count); + for (std::size_t i = 0; i < count; ++i) + keys.push_back(seq.key(i)); + return keys; +} + +// Mean payload size across a pool, used for SetBytesProcessed throughput. +inline std::size_t +averagePayload(Batch const& pool) +{ + if (pool.empty()) + return 0; + std::size_t total = 0; + for (auto const& obj : pool) + total += obj->getData().size(); + return total / pool.size(); +} + +// Store every object and flush, so a following fetch exercises the real read +// path rather than an in-memory write buffer. +// +// We chunk the write at kBatchWriteLimitSize because Types.h documents that as +// the maximum allowed batch size. NuDB happens to tolerate larger batches +// today, but the benchmark should not rely on that. +// +// sync() is a no-op for both NuDB and RocksDB at the moment (NuDB has a small +// internal burst buffer that the timed loop will warm up). That is a contract +// hint, not a guarantee; if either backend ever grows a real flush we get it +// here for free. +inline void +prepopulate(Backend& backend, Batch const& objects) +{ + for (std::size_t i = 0; i < objects.size(); i += kBatchWriteLimitSize) + { + auto const end = std::min(i + kBatchWriteLimitSize, objects.size()); + backend.storeBatch(Batch(objects.begin() + i, objects.begin() + end)); + } + backend.sync(); +} + +// A deterministic permutation of [0, size). Lets the timed loop visit the +// pre-generated pool in a random-like order with zero RNG cost per iteration - +// the Timing_test workloads it replaces used uniform_int_distribution per +// fetch, and a shuffle table reproduces that access pattern without paying for +// the distribution inside the timed region. +inline std::vector +makeShuffle(std::size_t size, std::uint64_t seed) +{ + std::vector v(size); + std::iota(v.begin(), v.end(), std::size_t{0}); + beast::xor_shift_engine gen(seed); + std::shuffle(v.begin(), v.end(), 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 +sliceBatches(Batch const& pool, std::size_t batchSize) +{ + std::vector batches; + if (batchSize == 0) + return batches; + batches.reserve(pool.size() / batchSize); + for (std::size_t i = 0; i + batchSize <= pool.size(); i += batchSize) + batches.emplace_back(pool.begin() + i, pool.begin() + i + batchSize); + return batches; +} + +/** + * @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; + DummyScheduler scheduler; + beast::Journal journal{beast::Journal::getNullSink()}; + std::unique_ptr backend; + + explicit BackendHarness(std::string const& configString) + { + Section config = parseConfig(configString); + // A private, unique path per harness, so concurrent or repeated runs + // never share on-disk state. + config.set("path", tempDir.path()); + backend = + Manager::instance().makeBackend(config, megabytes(std::size_t{4}), scheduler, journal); + backend->setDeletePath(); + backend->open(); + } + + ~BackendHarness() + { + if (backend) + backend->close(); + } +}; + +/** + * RAII owner of a NodeStore Database - the application-facing wrapper around a + * Backend, which adds fetch/store accounting and the async read-thread pool. + */ +struct DatabaseHarness +{ + beast::TempDir tempDir; + DummyScheduler scheduler; + beast::Journal journal{beast::Journal::getNullSink()}; + std::unique_ptr db; + + DatabaseHarness(std::string const& configString, int readThreads) + { + Section config = parseConfig(configString); + config.set("path", tempDir.path()); + db = Manager::instance().makeDatabase( + megabytes(std::size_t{4}), scheduler, readThreads, config, journal); + } + + ~DatabaseHarness() + { + if (db) + db->stop(); + } +}; + +// A NodeStore backend to benchmark, named for the --benchmark_filter CLI flag. +struct BackendConfig +{ + char const* name; // short label, e.g. "nudb" + char const* config; // parseConfig() string, e.g. "type=nudb" +}; + +// The backends every workload is registered against. +// +// The in-memory backend is intentionally excluded. It keeps its table in a +// process-global map keyed by path, with no removal API, so building a fresh +// backend per run - as a microbenchmark must - would leak the whole dataset on +// every run. Timing_test, the suite this benchmark replaces, excluded it for +// the same reason. NuDB and RocksDB are the production backends worth timing. +// +// RocksDB is included only when it was compiled in (xrpl.libxrpl carries +// XRPL_ROCKSDB_AVAILABLE transitively). +inline std::vector const& +backendConfigs() +{ + static std::vector 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"}, +#endif + }; + return kConfigs; +} + +} // namespace xrpl::NodeStore diff --git a/src/test/nodestore/Timing_test.cpp b/src/test/nodestore/Timing_test.cpp deleted file mode 100644 index 1f282d9d7f..0000000000 --- a/src/test/nodestore/Timing_test.cpp +++ /dev/null @@ -1,729 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef NODESTORE_TIMING_DO_VERIFY -#define NODESTORE_TIMING_DO_VERIFY 0 -#endif - -namespace xrpl::NodeStore { - -std::unique_ptr -makeBackend(Section const& config, Scheduler& scheduler, beast::Journal journal) -{ - return Manager::instance().makeBackend(config, megabytes(4), scheduler, journal); -} - -// Fill memory with random bits -template -static void -rngcpy(void* buffer, std::size_t bytes, Generator& g) -{ - using result_type = Generator::result_type; - while (bytes >= sizeof(result_type)) - { - auto const v = g(); - memcpy(buffer, &v, sizeof(v)); - buffer = reinterpret_cast(buffer) + sizeof(v); - bytes -= sizeof(v); - } - - if (bytes > 0) - { - auto const v = g(); - memcpy(buffer, &v, bytes); - } -} - -// Instance of node factory produces a deterministic sequence -// of random NodeObjects within the given -class Sequence -{ -private: - static constexpr auto kMinLedger = 1; - static constexpr auto kMaxLedger = 1000000; - static constexpr auto kMinSize = 250; - static constexpr auto kMaxSize = 1250; - - beast::xor_shift_engine gen_; - std::uint8_t prefix_; - std::discrete_distribution dType_; - std::uniform_int_distribution dSize_; - -public: - explicit Sequence(std::uint8_t prefix) - : prefix_(prefix) - // uniform distribution over hotLEDGER - hotTRANSACTION_NODE - // but exclude hotTRANSACTION = 2 (removed) - , dType_({1, 1, 0, 1, 1}) - , dSize_(kMinSize, kMaxSize) - { - } - - // Returns the n-th key - uint256 - key(std::size_t n) - { - gen_.seed(n + 1); - uint256 result; - rngcpy(&*result.begin(), result.size(), gen_); - return result; - } - - // Returns the n-th complete NodeObject - std::shared_ptr - obj(std::size_t n) - { - gen_.seed(n + 1); - uint256 key; - auto const data = static_cast(&*key.begin()); - *data = prefix_; - rngcpy(data + 1, key.size() - 1, gen_); - Blob value(dSize_(gen_)); - rngcpy(&value[0], value.size(), gen_); - return NodeObject::createObject( - safeCast(dType_(gen_)), std::move(value), key); - } - - // returns a batch of NodeObjects starting at n - void - batch(std::size_t n, Batch& b, std::size_t size) - { - b.clear(); - b.reserve(size); - while ((size--) != 0u) - b.emplace_back(obj(n++)); - } -}; - -//---------------------------------------------------------------------------------- - -class Timing_test : public beast::unit_test::Suite -{ -public: - static constexpr auto kMissingNodePercent = 20; // percent of fetches for missing nodes - - std::size_t const defaultRepeat = 3; -#ifndef NDEBUG - std::size_t const defaultItems = 10000; -#else - std::size_t const defaultItems = 100000; // release -#endif - - using clock_type = std::chrono::steady_clock; - using duration_type = std::chrono::milliseconds; - - struct Params - { - std::size_t items; - std::size_t threads; - }; - - static std::string - toString(Section const& config) - { - std::string s; - for (auto iter = config.begin(); iter != config.end(); ++iter) - s += (iter != config.begin() ? "," : "") + iter->first + "=" + iter->second; - return s; - } - - static std::string - toString(duration_type const& d) - { - std::stringstream ss; - ss << std::fixed << std::setprecision(3) << (d.count() / 1000.) << "s"; - return ss.str(); - } - - static Section - parse(std::string s) - { - Section section; - std::vector v; - boost::split(v, s, boost::algorithm::is_any_of(",")); - section.append(v); - return section; - } - - //-------------------------------------------------------------------------- - - // Workaround for GCC's parameter pack expansion in lambdas - // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47226 - template - class ParallelForLambda - { - private: - std::size_t const n_; - std::atomic& c_; - - public: - ParallelForLambda(std::size_t n, std::atomic& c) : n_(n), c_(c) - { - } - - template - void - operator()(Args&&... args) - { - Body body(args...); - for (;;) - { - auto const i = c_++; - if (i >= n_) - break; - body(i); - } - } - }; - - /* Execute parallel-for loop. - - Constructs `number_of_threads` instances of `Body` - with `args...` parameters and runs them on individual threads - with unique loop indexes in the range [0, n). - */ - template - void - parallelFor(std::size_t const n, std::size_t numberOfThreads, Args const&... args) - { - std::atomic c(0); - std::vector t; - t.reserve(numberOfThreads); - for (std::size_t id = 0; id < numberOfThreads; ++id) - t.emplace_back(*this, ParallelForLambda(n, c), args...); - for (auto& _ : t) - _.join(); - } - - template - void - parallelForId(std::size_t const n, std::size_t numberOfThreads, Args const&... args) - { - std::atomic c(0); - std::vector t; - t.reserve(numberOfThreads); - for (std::size_t id = 0; id < numberOfThreads; ++id) - t.emplace_back(*this, ParallelForLambda(n, c), id, args...); - for (auto& _ : t) - _.join(); - } - - //-------------------------------------------------------------------------- - - // Insert only - void - doInsert(Section const& config, Params const& params, beast::Journal journal) - { - DummyScheduler scheduler; - auto backend = makeBackend(config, scheduler, journal); - BEAST_EXPECT(backend != nullptr); - backend->open(); - - class Body - { - private: - Suite& suite_; - Backend& backend_; - Sequence seq_; - - public: - explicit Body(Suite& s, Backend& backend) : suite_(s), backend_(backend), seq_(1) - { - } - - void - operator()(std::size_t i) - { - try - { - backend_.store(seq_.obj(i)); - } - catch (std::exception const& e) - { - suite_.fail(e.what()); - } - } - }; - - try - { - parallelFor(params.items, params.threads, std::ref(*this), std::ref(*backend)); - } - catch (std::exception const&) - { -#if NODESTORE_TIMING_DO_VERIFY - backend->verify(); -#endif - rethrow(); - } - backend->close(); - } - - // Fetch existing keys - void - doFetch(Section const& config, Params const& params, beast::Journal journal) - { - DummyScheduler scheduler; - auto backend = makeBackend(config, scheduler, journal); - BEAST_EXPECT(backend != nullptr); - backend->open(); - - class Body - { - private: - Suite& suite_; - Backend& backend_; - Sequence seq1_; - beast::xor_shift_engine gen_; - std::uniform_int_distribution dist_; - - public: - Body(std::size_t id, Suite& s, Params const& params, Backend& backend) - : suite_(s), backend_(backend), seq1_(1), gen_(id + 1), dist_(0, params.items - 1) - { - } - - void - operator()(std::size_t i) - { - try - { - std::shared_ptr obj; - std::shared_ptr result; - obj = seq1_.obj(dist_(gen_)); - backend_.fetch(obj->getHash(), &result); - suite_.expect(result && isSame(result, obj)); - } - catch (std::exception const& e) - { - suite_.fail(e.what()); - } - } - }; - try - { - parallelForId( - params.items, - params.threads, - std::ref(*this), - std::ref(params), - std::ref(*backend)); - } - catch (std::exception const&) - { -#if NODESTORE_TIMING_DO_VERIFY - backend->verify(); -#endif - rethrow(); - } - backend->close(); - } - - // Perform lookups of non-existent keys - void - doMissing(Section const& config, Params const& params, beast::Journal journal) - { - DummyScheduler scheduler; - auto backend = makeBackend(config, scheduler, journal); - BEAST_EXPECT(backend != nullptr); - backend->open(); - - class Body - { - private: - Suite& suite_; - // Params const& params_; - Backend& backend_; - Sequence seq2_; - beast::xor_shift_engine gen_; - std::uniform_int_distribution dist_; - - public: - Body(std::size_t id, Suite& s, Params const& params, Backend& backend) - : suite_(s) - //, params_ (params) - , backend_(backend) - , seq2_(2) - , gen_(id + 1) - , dist_(0, params.items - 1) - { - } - - void - operator()(std::size_t i) - { - try - { - auto const hash = seq2_.key(i); - std::shared_ptr result; - backend_.fetch(hash, &result); - suite_.expect(!result); - } - catch (std::exception const& e) - { - suite_.fail(e.what()); - } - } - }; - - try - { - parallelForId( - params.items, - params.threads, - std::ref(*this), - std::ref(params), - std::ref(*backend)); - } - catch (std::exception const&) - { -#if NODESTORE_TIMING_DO_VERIFY - backend->verify(); -#endif - rethrow(); - } - backend->close(); - } - - // Fetch with present and missing keys - void - doMixed(Section const& config, Params const& params, beast::Journal journal) - { - DummyScheduler scheduler; - auto backend = makeBackend(config, scheduler, journal); - BEAST_EXPECT(backend != nullptr); - backend->open(); - - class Body - { - private: - Suite& suite_; - // Params const& params_; - Backend& backend_; - Sequence seq1_; - Sequence seq2_; - beast::xor_shift_engine gen_; - std::uniform_int_distribution rand_; - std::uniform_int_distribution dist_; - - public: - Body(std::size_t id, Suite& s, Params const& params, Backend& backend) - : suite_(s) - //, params_ (params) - , backend_(backend) - , seq1_(1) - , seq2_(2) - , gen_(id + 1) - , rand_(0, 99) - , dist_(0, params.items - 1) - { - } - - void - operator()(std::size_t i) - { - try - { - if (rand_(gen_) < kMissingNodePercent) - { - auto const hash = seq2_.key(dist_(gen_)); - std::shared_ptr result; - backend_.fetch(hash, &result); - suite_.expect(!result); - } - else - { - std::shared_ptr obj; - std::shared_ptr result; - obj = seq1_.obj(dist_(gen_)); - backend_.fetch(obj->getHash(), &result); - suite_.expect(result && isSame(result, obj)); - } - } - catch (std::exception const& e) - { - suite_.fail(e.what()); - } - } - }; - - try - { - parallelForId( - params.items, - params.threads, - std::ref(*this), - std::ref(params), - std::ref(*backend)); - } - catch (std::exception const&) - { -#if NODESTORE_TIMING_DO_VERIFY - backend->verify(); -#endif - rethrow(); - } - backend->close(); - } - - // Simulate an xrpld workload: - // Each thread randomly: - // inserts a new key - // fetches an old key - // fetches recent, possibly non existent data - void - doWork(Section const& config, Params const& params, beast::Journal journal) - { - DummyScheduler scheduler; - auto backend = makeBackend(config, scheduler, journal); - BEAST_EXPECT(backend != nullptr); - backend->setDeletePath(); - backend->open(); - - class Body - { - private: - Suite& suite_; - Params const& params_; - Backend& backend_; - Sequence seq1_; - beast::xor_shift_engine gen_; - std::uniform_int_distribution rand_; - std::uniform_int_distribution recent_; - std::uniform_int_distribution older_; - - public: - Body(std::size_t id, Suite& s, Params const& params, Backend& backend) - : suite_(s) - , params_(params) - , backend_(backend) - , seq1_(1) - , gen_(id + 1) - , rand_(0, 99) - , recent_(params.items, (params.items * 2) - 1) - , older_(0, params.items - 1) - { - } - - void - operator()(std::size_t i) - { - try - { - if (rand_(gen_) < 200) - { - // historical lookup - std::shared_ptr obj; - std::shared_ptr result; - auto const j = older_(gen_); - obj = seq1_.obj(j); - backend_.fetch(obj->getHash(), &result); - suite_.expect(result != nullptr); - suite_.expect(isSame(result, obj)); - } - - char p[2]; - p[0] = rand_(gen_) < 50 ? 0 : 1; - p[1] = 1 - p[0]; - for (char const op : p) - { - // NOLINTNEXTLINE(bugprone-switch-missing-default-case) - switch (op) - { - case 0: { - // fetch recent - std::shared_ptr obj; - std::shared_ptr result; - auto const j = recent_(gen_); - obj = seq1_.obj(j); - backend_.fetch(obj->getHash(), &result); - suite_.expect(!result || isSame(result, obj)); - break; - } - - case 1: { - // insert new - auto const j = i + params_.items; - backend_.store(seq1_.obj(j)); - break; - } - } - } - } - catch (std::exception const& e) - { - suite_.fail(e.what()); - } - } - }; - - try - { - parallelForId( - params.items, - params.threads, - std::ref(*this), - std::ref(params), - std::ref(*backend)); - } - catch (std::exception const&) - { -#if NODESTORE_TIMING_DO_VERIFY - backend->verify(); -#endif - rethrow(); - } - backend->close(); - } - - //-------------------------------------------------------------------------- - - using test_func = void (Timing_test::*)(Section const&, Params const&, beast::Journal); - using test_list = std::vector>; - - duration_type - doTest(test_func f, Section const& config, Params const& params, beast::Journal journal) - { - auto const start = clock_type::now(); - (this->*f)(config, params, journal); - return std::chrono::duration_cast(clock_type::now() - start); - } - - void - doTests( - std::size_t threads, - test_list const& tests, - std::vector const& configStrings) - { - using std::setw; - int w = 8; - for (auto const& test : tests) - { - w = std::max::size_type>(w, test.first.size()); - } - log << threads << " Thread" << (threads > 1 ? "s" : "") << ", " << defaultItems - << " Objects" << std::endl; - { - std::stringstream ss; - ss << std::left << setw(10) << "Backend" << std::right; - for (auto const& test : tests) - ss << " " << setw(w) << test.first; - log << ss.str() << std::endl; - } - - using beast::Severity; - test::SuiteJournal journal("Timing_test", *this); - - for (auto const& configString : configStrings) - { - Params params{}; - params.items = defaultItems; - params.threads = threads; - for (auto i = defaultRepeat; (i--) != 0u;) - { - beast::TempDir const tempDir; - Section config = parse(configString); - config.set(Keys::kPath, tempDir.path()); - std::stringstream ss; - ss << std::left << setw(10) << get(config, Keys::kType, std::string()) - << std::right; - for (auto const& test : tests) - { - ss << " " << setw(w) << toString(doTest(test.second, config, params, journal)); - } - ss << " " << toString(config); - log << ss.str() << std::endl; - } - } - } - - void - run() override - { - testcase("Timing", beast::unit_test::AbortT::AbortOnFail); - - /* Parameters: - - repeat Number of times to repeat each test - items Number of objects to create in the database - - */ - std::string const defaultArgs = - "type=nudb" -#if XRPL_ROCKSDB_AVAILABLE - ";type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256," - "file_size_mb=8,file_size_mult=2" -#endif - ; - - test_list const tests = { - {"Insert", &Timing_test::doInsert}, - {"Fetch", &Timing_test::doFetch}, - {"Missing", &Timing_test::doMissing}, - {"Mixed", &Timing_test::doMixed}, - {"Work", &Timing_test::doWork}}; - - auto args = arg().empty() ? defaultArgs : arg(); - std::vector configStrings; - boost::split(configStrings, args, boost::algorithm::is_any_of(";")); - for (auto iter = configStrings.begin(); iter != configStrings.end();) - { - if (iter->empty()) - { - iter = configStrings.erase(iter); - } - else - { - ++iter; - } - } - - doTests(1, tests, configStrings); - doTests(4, tests, configStrings); - doTests(8, tests, configStrings); - // do_tests (16, tests, config_strings); - } -}; - -BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Timing, nodestore, xrpl, 1); - -} // namespace xrpl::NodeStore