xrpld
Loading...
Searching...
No Matches
beast_io_latency_probe_test.cpp
1#include <xrpl/beast/asio/io_latency_probe.h>
2#include <xrpl/beast/test/yield_to.h>
3#include <xrpl/beast/unit_test/suite.h>
4
5#include <boost/asio/basic_waitable_timer.hpp>
6#include <boost/asio/executor_work_guard.hpp> // IWYU pragma: keep
7#include <boost/asio/io_context.hpp>
8#include <boost/asio/spawn.hpp>
9#include <boost/system/detail/error_code.hpp>
10
11#include <algorithm>
12#include <chrono>
13#include <condition_variable> // IWYU pragma: keep
14#include <cstddef>
15#include <functional>
16#include <mutex> // IWYU pragma: keep
17#include <optional> // IWYU pragma: keep
18#include <stdexcept>
19#include <string>
20#include <thread> // IWYU pragma: keep
21#include <vector>
22
23using namespace std::chrono_literals;
24
26{
27 using MyTimer = boost::asio::basic_waitable_timer<std::chrono::steady_clock>;
28
29#ifdef XRPL_RUNNING_IN_CI
37 template <class Clock, class MeasureClock = std::chrono::high_resolution_clock>
38 struct MeasureAsioTimers
39 {
40 using duration = Clock::duration;
41 using rep = MeasureClock::duration::rep;
42
43 std::vector<duration> elapsedTimes;
44
45 MeasureAsioTimers(duration interval = 100ms, size_t numSamples = 50)
46 {
47 using namespace std::chrono;
48 boost::asio::io_context ios;
50 work{boost::asio::make_work_guard(ios)};
51 std::thread worker{[&] { ios.run(); }};
52 boost::asio::basic_waitable_timer<Clock> timer{ios};
53 elapsedTimes.reserve(numSamples);
54 std::mutex mtx;
55 std::unique_lock<std::mutex> mainlock{mtx};
57 bool done = false;
58 boost::system::error_code waitErr;
59
60 while (--numSamples > 0u)
61 {
62 auto const start{MeasureClock::now()};
63 done = false;
64 timer.expires_after(interval);
65 timer.async_wait([&](boost::system::error_code const& ec) {
66 if (ec)
67 waitErr = ec;
68 auto const end{MeasureClock::now()};
69 elapsedTimes.emplace_back(end - start);
70 std::scoped_lock const lk{mtx};
71 done = true;
72 cv.notify_one();
73 });
74 cv.wait(mainlock, [&done] { return done; });
75 }
76 work.reset();
77 worker.join();
78 if (waitErr)
79 boost::asio::detail::throw_error(waitErr, "wait");
80 }
81
82 template <class D>
83 auto
84 getMean()
85 {
86 double sum = {0};
87 for (auto const& v : elapsedTimes)
88 {
89 sum += static_cast<double>(std::chrono::duration_cast<D>(v).count());
90 }
91 return sum / elapsedTimes.size();
92 }
93
94 template <class D>
95 auto
96 getMax()
97 {
98 return std::chrono::duration_cast<D>(*std::ranges::max_element(elapsedTimes)).count();
99 }
100
101 template <class D>
102 auto
103 getMin()
104 {
105 return std::chrono::duration_cast<D>(*std::ranges::min_element(elapsedTimes)).count();
106 }
107 };
108#endif
109
111 {
114
115 TestSampler(std::chrono::milliseconds interval, boost::asio::io_context& ios)
116 : probe(interval, ios)
117 {
118 }
119
120 void
122 {
123 probe.sample(std::ref(*this));
124 }
125
126 void
128 {
129 probe.sampleOne(std::ref(*this));
130 }
131
132 void
133 operator()(std::chrono::steady_clock::duration const& elapsed)
134 {
135 durations.push_back(elapsed);
136 }
137 };
138
139 void
140 testSampleOne(boost::asio::yield_context& yield)
141 {
142 testcase << "sample one";
143 boost::system::error_code ec;
144 TestSampler ioProbe{100ms, getIoContext()};
145 ioProbe.startOne();
146 MyTimer timer{getIoContext(), 1s};
147 timer.async_wait(yield[ec]);
148 if (!BEAST_EXPECTS(!ec, ec.message()))
149 return;
150 BEAST_EXPECT(ioProbe.durations.size() == 1);
151 ioProbe.probe.cancelAsync();
152 }
153
154 void
155 testSampleOngoing(boost::asio::yield_context& yield)
156 {
157 testcase << "sample ongoing";
158 boost::system::error_code ec;
159 using namespace std::chrono;
160 auto interval = 99ms;
161 auto probeDuration = 1s;
162
163 size_t const expectedProbeCountMax = (probeDuration / interval);
164 // NOLINTNEXTLINE(misc-const-correctness)
165 size_t expectedProbeCountMin = expectedProbeCountMax;
166#ifdef XRPL_RUNNING_IN_CI
167 // adjust min expected based on measurements
168 // if running in CI/VM environment
169 MeasureAsioTimers<steady_clock> tt{interval};
170 log << "measured mean for timers: " << tt.getMean<milliseconds>() << "ms\n";
171 log << "measured max for timers: " << tt.getMax<milliseconds>() << "ms\n";
172 expectedProbeCountMin =
173 static_cast<size_t>(duration_cast<milliseconds>(probeDuration).count()) /
174 static_cast<size_t>(tt.getMean<milliseconds>());
175#endif
176 TestSampler ioProbe{interval, getIoContext()};
177 ioProbe.start();
178 MyTimer timer{getIoContext(), probeDuration};
179 timer.async_wait(yield[ec]);
180 if (!BEAST_EXPECTS(!ec, ec.message()))
181 return;
182 auto probesSeen = ioProbe.durations.size();
183 BEAST_EXPECTS(
184 probesSeen >= (expectedProbeCountMin - 1) && probesSeen <= (expectedProbeCountMax + 1),
185 std::string("probe count is ") + std::to_string(probesSeen));
186 ioProbe.probe.cancelAsync();
187 // wait again in order to flush the remaining
188 // probes from the work queue
189 timer.expires_after(1s);
190 timer.async_wait(yield[ec]);
191 }
192
193 void
194 testCanceled(boost::asio::yield_context& yield)
195 {
196 testcase << "canceled";
197 TestSampler ioProbe{100ms, getIoContext()};
198 ioProbe.probe.cancelAsync();
199 except<std::logic_error>([&ioProbe]() { ioProbe.startOne(); });
200 except<std::logic_error>([&ioProbe]() { ioProbe.start(); });
201 }
202
203public:
204 void
205 run() override
206 {
207 yieldTo([&](boost::asio::yield_context& yield) {
208 testSampleOne(yield);
209 testSampleOngoing(yield);
210 testCanceled(yield);
211 });
212 }
213};
214
215BEAST_DEFINE_TESTSUITE(io_latency_probe, beast, beast);
Measures handler latency on an io_context queue.
Mix-in to support tests using asio coroutines.
Definition yield_to.h:27
boost::asio::io_context & getIoContext()
Return the io_context associated with the object.
Definition yield_to.h:60
void yieldTo(F0 &&f0, FN &&... fn)
Run one or more functions, each in a coroutine.
Definition yield_to.h:99
A testsuite class.
Definition suite.h:50
bool except(F &&f, String const &reason)
Definition suite.h:433
LogOs< char > log
Logging output stream.
Definition suite.h:146
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
boost::asio::basic_waitable_timer< std::chrono::steady_clock > MyTimer
void testCanceled(boost::asio::yield_context &yield)
void testSampleOngoing(boost::asio::yield_context &yield)
void run() override
Runs the suite.
void testSampleOne(boost::asio::yield_context &yield)
T duration_cast(T... args)
T emplace_back(T... args)
T end(T... args)
T max_element(T... args)
T min_element(T... args)
T ref(T... args)
T reserve(T... args)
T reset(T... args)
T size(T... args)
TestSampler(std::chrono::milliseconds interval, boost::asio::io_context &ios)
std::vector< std::chrono::steady_clock::duration > durations
void operator()(std::chrono::steady_clock::duration const &elapsed)
beast::IOLatencyProbe< std::chrono::steady_clock > probe
T to_string(T... args)