xrpld
Toggle main menu visibility
Loading...
Searching...
No Matches
test
rpc
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
23
namespace
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
29
std::ostream
&
30
operator<<
(
std::ostream
& os,
std::chrono::nanoseconds
ns)
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
41
class
Handler_test
:
public
beast::unit_test::Suite
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
87
reportLookupPerformance
()
88
{
89
testcase
(
"Handler lookup performance"
);
90
91
std::random_device
dev;
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.
102
std::vector<std::string_view>
names;
103
std::ranges::copy_if
(
104
rpc::getHandlerNames
(),
std::back_inserter
(names), [](
std::string_view
name) {
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
114
std::uniform_int_distribution<std::size_t>
distr{0, names.
size
() - 1};
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
141
public
:
142
void
143
run
()
override
144
{
145
reportLookupPerformance
();
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.
151
BEAST_DEFINE_TESTSUITE_MANUAL
(Handler,
rpc
,
xrpl
);
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.
160
class
HandlerLookup_test
:
public
beast::unit_test::Suite
161
{
172
static
std::optional<std::string_view>
173
nameServedAt
(
unsigned
version,
bool
betaEnabled)
174
{
175
for
(
std::string_view
name :
rpc::getHandlerNames
())
176
{
177
if
(
rpc::getHandler
(version, betaEnabled, name) !=
nullptr
)
178
return
name;
179
}
180
181
return
std::nullopt;
182
}
183
184
void
185
testUnservedVersion
()
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 "
+
195
std::to_string
(
rpc::kApiMinimumSupportedVersion
) +
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
213
testBetaVersionGate
()
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.
220
if
constexpr
(
rpc::kApiBetaVersion
>
rpc::kApiMaximumSupportedVersion
)
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
243
testUnknownMethod
()
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
258
public
:
259
void
260
run
()
override
261
{
262
testUnservedVersion
();
263
testBetaVersionGate
();
264
testUnknownMethod
();
265
}
266
};
267
268
BEAST_DEFINE_TESTSUITE
(HandlerLookup,
rpc
,
xrpl
);
269
270
}
// namespace xrpl::test
algorithm
array
std::back_inserter
T back_inserter(T... args)
std::ostream
std::string_view
cassert
chrono
beast::unit_test::Suite
A testsuite class.
Definition
suite.h:52
beast::unit_test::Suite::pass
void pass()
Record a successful test condition.
Definition
suite.h:532
beast::unit_test::Suite::log
LogOs< char > log
Logging output stream.
Definition
suite.h:150
beast::unit_test::Suite::testcase
TestcaseT testcase
Memberspace for declaring test cases.
Definition
suite.h:155
xrpl::test::HandlerLookup_test
Definition
Handler_test.cpp:161
xrpl::test::HandlerLookup_test::testUnservedVersion
void testUnservedVersion()
Definition
Handler_test.cpp:185
xrpl::test::HandlerLookup_test::testBetaVersionGate
void testBetaVersionGate()
Definition
Handler_test.cpp:213
xrpl::test::HandlerLookup_test::nameServedAt
static std::optional< std::string_view > nameServedAt(unsigned version, bool betaEnabled)
Find a method that is served at a given API version.
Definition
Handler_test.cpp:173
xrpl::test::HandlerLookup_test::run
void run() override
Runs the suite.
Definition
Handler_test.cpp:260
xrpl::test::HandlerLookup_test::testUnknownMethod
void testUnknownMethod()
Definition
Handler_test.cpp:243
xrpl::test::Handler_test
Definition
Handler_test.cpp:42
xrpl::test::Handler_test::run
void run() override
Runs the suite.
Definition
Handler_test.cpp:143
xrpl::test::Handler_test::time
auto time(std::size_t n, auto f, auto prng) -> auto
Definition
Handler_test.cpp:44
xrpl::test::Handler_test::reportLookupPerformance
void reportLookupPerformance()
Definition
Handler_test.cpp:87
cmath
std::ranges::copy_if
T copy_if(T... args)
std::chrono::nanoseconds::count
T count(T... args)
std::cout
cstddef
std::ranlux48
std::chrono::nanoseconds
std::vector::empty
T empty(T... args)
iostream
iterator
std::make_tuple
T make_tuple(T... args)
xrpl::rpc
API version numbers used in later API versions.
Definition
ApiVersion.h:36
xrpl::rpc::getHandlerNames
std::span< NullTerminatedView const > getHandlerNames()
Return the names of all methods, sorted and without duplicates.
Definition
Handler.cpp:691
xrpl::rpc::kApiMaximumSupportedVersion
static constexpr auto kApiMaximumSupportedVersion
Definition
ApiVersion.h:43
xrpl::rpc::getHandler
Handler const * getHandler(unsigned version, bool betaEnabled, std::string_view name)
Definition
Handler.cpp:672
xrpl::rpc::kApiMinimumSupportedVersion
static constexpr auto kApiMinimumSupportedVersion
Definition
ApiVersion.h:42
xrpl::rpc::kApiBetaVersion
static constexpr auto kApiBetaVersion
Definition
ApiVersion.h:46
xrpl::test::jtx::operator<<
std::ostream & operator<<(std::ostream &os, PrettyAmount const &amount)
Definition
amount.cpp:46
xrpl::test
Definition
STLedgerEntry.h:21
xrpl::test::BEAST_DEFINE_TESTSUITE
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
xrpl::test::BEAST_DEFINE_TESTSUITE_MANUAL
BEAST_DEFINE_TESTSUITE_MANUAL(AMMCalc, app, xrpl)
xrpl
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition
algorithm.h:5
xrpl::sum
static auto sum(TCollection const &col)
Definition
BookStep.cpp:1058
std::chrono::steady_clock::now
T now(T... args)
optional
std::random_device
random
std::vector::size
T size(T... args)
std::size_t
std::ranges::sort
T sort(T... args)
std::sqrt
T sqrt(T... args)
std::chrono::steady_clock
string
string_view
std::to_string
T to_string(T... args)
tuple
std::uniform_int_distribution
vector
Generated by
1.17.0