xrpld
Loading...
Searching...
No Matches
Handler_test.cpp
1
2#include <xrpld/rpc/detail/Handler.h>
3
4#include <xrpl/beast/unit_test/suite.h>
5#include <xrpl/protocol/ApiVersion.h>
6
7#include <algorithm>
8#include <array>
9#include <cassert>
10#include <chrono>
11#include <cmath>
12#include <cstddef>
13#include <iostream>
14#include <iterator>
15#include <optional>
16#include <random>
17#include <string>
18#include <string_view>
19#include <tuple>
20#include <vector>
21// cspell: words stdev
22
23namespace xrpl::test {
24
25// NOTE: there should be no need for this function;
26// `std::cout << some_duration` should just work if built with a compliant
27// C++20 compiler. Sadly, we are not using one, as of today
28// TODO: remove this operator<< overload when we bump compiler version
31{
32 return (os << ns.count() << "ns");
33}
34
35// NOTE This is a rather naive effort at a microbenchmark. Ideally we want
36// Google Benchmark, or something similar. Also, this actually does not belong
37// to unit tests, as it makes little sense to run it in conditions very
38// dissimilar to how xrpld will normally work.
39// TODO as https://github.com/XRPLF/rippled/issues/4765
40
42{
43 auto
44 time(std::size_t n, auto f, auto prng) -> auto
45 {
46 using Clock = std::chrono::steady_clock;
47 assert(n > 0);
48 double sum = 0;
49 double sumSquared = 0;
50 std::size_t j = 0;
51 while (j < n)
52 {
53 // Generate 100 inputs upfront, separated from the inner loop
54 std::array<decltype(prng()), 100> inputs = {};
55 for (auto& i : inputs)
56 {
57 i = prng();
58 }
59
60 // Take 100 samples, then sort and throw away 35 from each end,
61 // using only middle 30. This helps to reduce measurement noise.
62 std::array<long, 100> samples = {};
63 for (std::size_t k = 0; k < 100; ++k)
64 {
65 auto start = std::chrono::steady_clock::now();
66 f(inputs[k]);
67 samples[k] = (std::chrono::steady_clock::now() - start).count();
68 }
69
70 std::ranges::sort(samples);
71 for (std::size_t k = 35; k < 65; ++k)
72 {
73 j += 1;
74 sum += samples[k];
75 sumSquared += (samples[k] * samples[k]);
76 }
77 }
78
79 double const meanSquared = (sum * sum) / (j * j);
80 return std::make_tuple(
81 Clock::duration{static_cast<long>(sum / j)},
82 Clock::duration{static_cast<long>(std::sqrt((sumSquared / j) - meanSquared))},
83 j);
84 }
85
86 void
88 {
89 testcase("Handler lookup performance");
90
92 std::ranlux48 prng(dev());
93
94 // The lowest version still served. Outside the supported range getHandler()
95 // returns at its bounds check without searching, so the benchmark would
96 // time that check instead of a lookup.
97 constexpr unsigned kVersion = rpc::kApiMinimumSupportedVersion;
98
99 // Only the names that answer at kVersion, so that every timed call does a
100 // whole lookup: a method served from a later version only would not.
101 // Contiguous, so that picking one by index costs nothing.
105 return rpc::getHandler(kVersion, false, name) != nullptr;
106 });
107
108 if (!BEAST_EXPECTS(
109 !names.empty(),
110 "no handler answers at API version " + std::to_string(kVersion) +
111 ", so there is nothing to measure"))
112 return;
113
115
116 std::size_t dummy = 0;
117 std::size_t misses = 0;
118 auto const [mean, stdev, n] = time(
119 1'000'000,
120 [&](std::size_t i) {
121 auto const d = rpc::getHandler(kVersion, false, names[i]);
122 if (d == nullptr)
123 {
124 ++misses;
125 return;
126 }
127 dummy = dummy + i + (int)d->role;
128 },
129 [&]() -> std::size_t { return distr(prng); });
130
131 std::cout << "mean=" << mean << " stdev=" << stdev << " N=" << n << '\n';
132
133 // Every name answered once already, so a miss here cannot happen.
134 BEAST_EXPECTS(
135 misses == 0,
136 std::to_string(misses) + " of " + std::to_string(n) + " lookups at API version " +
137 std::to_string(kVersion) + " found no handler, so nothing was measured");
138 BEAST_EXPECT(dummy != 0);
139 }
140
141public:
142 void
143 run() override
144 {
146 }
147};
148
149// Manual: the suite only reports a timing, which says nothing on a CI runner.
150// The table invariants are static_asserts in Handler.cpp.
152
153// What getHandler() answers, as opposed to how fast it answers. A lookup needs no
154// Application, so these cases run as an automatic suite.
155//
156// The bounds check they cover is unreachable from a request: getAPIVersionNumber()
157// applies the same predicate first, and every caller rejects an invalid version
158// before it asks for a handler. That is why it is checked here directly, and why
159// it is worth checking at all rather than deleting as unreachable.
161{
173 nameServedAt(unsigned version, bool betaEnabled)
174 {
176 {
177 if (rpc::getHandler(version, betaEnabled, name) != nullptr)
178 return name;
179 }
180
181 return std::nullopt;
182 }
183
184 void
186 {
187 testcase("An unserved API version has no handler");
188
189 // A name the table certainly holds, so that a null answer below can only
190 // come from the version and not from the name.
191 auto const name = nameServedAt(rpc::kApiMinimumSupportedVersion, false);
192 if (!BEAST_EXPECTS(
193 name.has_value(),
194 "no handler answers at API version " +
196 ", so there is no name to ask about"))
197 return;
198
199 // Below the minimum, which no setting serves.
200 BEAST_EXPECT(
201 rpc::getHandler(rpc::kApiMinimumSupportedVersion - 1, false, *name) == nullptr);
202 BEAST_EXPECT(rpc::getHandler(rpc::kApiMinimumSupportedVersion - 1, true, *name) == nullptr);
203
204 // Above the maximum each setting serves. Both values stay outside the
205 // served range however the version constants move, so neither case can
206 // become vacuous.
207 BEAST_EXPECT(
208 rpc::getHandler(rpc::kApiMaximumSupportedVersion + 1, false, *name) == nullptr);
209 BEAST_EXPECT(rpc::getHandler(rpc::kApiBetaVersion + 1, true, *name) == nullptr);
210 }
211
212 void
214 {
215 testcase("The beta API version is served only where it is enabled");
216
217 // Between betas the beta version is the maximum supported one, leaving the
218 // two settings nothing to tell apart. Compiled out rather than asserted, so
219 // that the case arms itself again when a later beta version arrives.
221 {
222 auto const name = nameServedAt(rpc::kApiBetaVersion, true);
223 if (!BEAST_EXPECTS(
224 name.has_value(),
225 "no handler answers at API version " + std::to_string(rpc::kApiBetaVersion) +
226 ", so there is nothing for the gate to reject"))
227 return;
228
229 // The handler serves this version, so only the server's own range can
230 // turn the answer into a null one.
231 BEAST_EXPECT(rpc::getHandler(rpc::kApiBetaVersion, true, *name) != nullptr);
232 BEAST_EXPECT(rpc::getHandler(rpc::kApiBetaVersion, false, *name) == nullptr);
233 }
234 else
235 {
236 log << "the beta API version is the maximum supported version, so no gate "
237 "separates them\n";
238 pass();
239 }
240 }
241
242 void
244 {
245 testcase("An unknown method has no handler");
246
247 constexpr unsigned kVersion = rpc::kApiMinimumSupportedVersion;
248
249 BEAST_EXPECT(rpc::getHandler(kVersion, false, "no such method") == nullptr);
250 BEAST_EXPECT(rpc::getHandler(kVersion, false, "") == nullptr);
251
252 // A method name holds lowercase letters and underscores, so a tilde sorts
253 // after every entry. This runs the search off the end of the table, which
254 // no other case here does.
255 BEAST_EXPECT(rpc::getHandler(kVersion, false, "~") == nullptr);
256 }
257
258public:
259 void
260 run() override
261 {
265 }
266};
267
269
270} // namespace xrpl::test
T back_inserter(T... args)
A testsuite class.
Definition suite.h:52
void pass()
Record a successful test condition.
Definition suite.h:532
LogOs< char > log
Logging output stream.
Definition suite.h:150
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
static std::optional< std::string_view > nameServedAt(unsigned version, bool betaEnabled)
Find a method that is served at a given API version.
void run() override
Runs the suite.
void run() override
Runs the suite.
auto time(std::size_t n, auto f, auto prng) -> auto
T copy_if(T... args)
T empty(T... args)
T make_tuple(T... args)
API version numbers used in later API versions.
Definition ApiVersion.h:36
std::span< NullTerminatedView const > getHandlerNames()
Return the names of all methods, sorted and without duplicates.
Definition Handler.cpp:691
static constexpr auto kApiMaximumSupportedVersion
Definition ApiVersion.h:43
Handler const * getHandler(unsigned version, bool betaEnabled, std::string_view name)
Definition Handler.cpp:672
static constexpr auto kApiMinimumSupportedVersion
Definition ApiVersion.h:42
static constexpr auto kApiBetaVersion
Definition ApiVersion.h:46
std::ostream & operator<<(std::ostream &os, PrettyAmount const &amount)
Definition amount.cpp:46
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
BEAST_DEFINE_TESTSUITE_MANUAL(AMMCalc, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
static auto sum(TCollection const &col)
T size(T... args)
T sort(T... args)
T sqrt(T... args)
T to_string(T... args)