rippled
Loading...
Searching...
No Matches
beast_io_latency_probe_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2018 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpl/beast/asio/io_latency_probe.h>
21#include <xrpl/beast/test/yield_to.h>
22#include <xrpl/beast/unit_test.h>
23
24#include <boost/asio/basic_waitable_timer.hpp>
25#include <boost/asio/deadline_timer.hpp>
26#include <boost/asio/executor_work_guard.hpp>
27#include <boost/asio/io_context.hpp>
28
29#include <algorithm>
30#include <mutex>
31#include <numeric>
32#include <optional>
33#include <vector>
34
35using namespace std::chrono_literals;
36
39{
40 using MyTimer =
41 boost::asio::basic_waitable_timer<std::chrono::steady_clock>;
42
43#ifdef RIPPLED_RUNNING_IN_CI
51 template <
52 class Clock,
53 class MeasureClock = std::chrono::high_resolution_clock>
54 struct measure_asio_timers
55 {
56 using duration = typename Clock::duration;
57 using rep = typename MeasureClock::duration::rep;
58
59 std::vector<duration> elapsed_times_;
60
61 measure_asio_timers(duration interval = 100ms, size_t num_samples = 50)
62 {
63 using namespace std::chrono;
64 boost::asio::io_context ios;
65 std::optional<boost::asio::executor_work_guard<
66 boost::asio::io_context::executor_type>>
67 work{boost::asio::make_work_guard(ios)};
68 std::thread worker{[&] { ios.run(); }};
69 boost::asio::basic_waitable_timer<Clock> timer{ios};
70 elapsed_times_.reserve(num_samples);
71 std::mutex mtx;
72 std::unique_lock<std::mutex> mainlock{mtx};
74 bool done = false;
75 boost::system::error_code wait_err;
76
77 while (--num_samples)
78 {
79 auto const start{MeasureClock::now()};
80 done = false;
81 timer.expires_after(interval);
82 timer.async_wait([&](boost::system::error_code const& ec) {
83 if (ec)
84 wait_err = ec;
85 auto const end{MeasureClock::now()};
86 elapsed_times_.emplace_back(end - start);
87 std::lock_guard lk{mtx};
88 done = true;
89 cv.notify_one();
90 });
91 cv.wait(mainlock, [&done] { return done; });
92 }
93 work.reset();
94 worker.join();
95 if (wait_err)
96 boost::asio::detail::throw_error(wait_err, "wait");
97 }
98
99 template <class D>
100 auto
101 getMean()
102 {
103 double sum = {0};
104 for (auto const& v : elapsed_times_)
105 {
106 sum += static_cast<double>(
107 std::chrono::duration_cast<D>(v).count());
108 }
109 return sum / elapsed_times_.size();
110 }
111
112 template <class D>
113 auto
114 getMax()
115 {
116 return std::chrono::duration_cast<D>(
118 elapsed_times_.begin(), elapsed_times_.end()))
119 .count();
120 }
121
122 template <class D>
123 auto
124 getMin()
125 {
126 return std::chrono::duration_cast<D>(
128 elapsed_times_.begin(), elapsed_times_.end()))
129 .count();
130 }
131 };
132#endif
133
135 {
138
141 boost::asio::io_context& ios)
142 : probe_(interval, ios)
143 {
144 }
145
146 void
148 {
149 probe_.sample(std::ref(*this));
150 }
151
152 void
154 {
155 probe_.sample_one(std::ref(*this));
156 }
157
158 void
159 operator()(std::chrono::steady_clock::duration const& elapsed)
160 {
161 durations_.push_back(elapsed);
162 }
163 };
164
165 void
166 testSampleOne(boost::asio::yield_context& yield)
167 {
168 testcase << "sample one";
169 boost::system::error_code ec;
170 test_sampler io_probe{100ms, get_io_context()};
171 io_probe.start_one();
172 MyTimer timer{get_io_context(), 1s};
173 timer.async_wait(yield[ec]);
174 if (!BEAST_EXPECTS(!ec, ec.message()))
175 return;
176 BEAST_EXPECT(io_probe.durations_.size() == 1);
177 io_probe.probe_.cancel_async();
178 }
179
180 void
181 testSampleOngoing(boost::asio::yield_context& yield)
182 {
183 testcase << "sample ongoing";
184 boost::system::error_code ec;
185 using namespace std::chrono;
186 auto interval = 99ms;
187 auto probe_duration = 1s;
188
189 size_t expected_probe_count_max = (probe_duration / interval);
190 size_t expected_probe_count_min = expected_probe_count_max;
191#ifdef RIPPLED_RUNNING_IN_CI
192 // adjust min expected based on measurements
193 // if running in CI/VM environment
194 measure_asio_timers<steady_clock> tt{interval};
195 log << "measured mean for timers: " << tt.getMean<milliseconds>()
196 << "ms\n";
197 log << "measured max for timers: " << tt.getMax<milliseconds>()
198 << "ms\n";
199 expected_probe_count_min =
200 static_cast<size_t>(
201 duration_cast<milliseconds>(probe_duration).count()) /
202 static_cast<size_t>(tt.getMean<milliseconds>());
203#endif
204 test_sampler io_probe{interval, get_io_context()};
205 io_probe.start();
206 MyTimer timer{get_io_context(), probe_duration};
207 timer.async_wait(yield[ec]);
208 if (!BEAST_EXPECTS(!ec, ec.message()))
209 return;
210 auto probes_seen = io_probe.durations_.size();
211 BEAST_EXPECTS(
212 probes_seen >= (expected_probe_count_min - 1) &&
213 probes_seen <= (expected_probe_count_max + 1),
214 std::string("probe count is ") + std::to_string(probes_seen));
215 io_probe.probe_.cancel_async();
216 // wait again in order to flush the remaining
217 // probes from the work queue
218 timer.expires_after(1s);
219 timer.async_wait(yield[ec]);
220 }
221
222 void
223 testCanceled(boost::asio::yield_context& yield)
224 {
225 testcase << "canceled";
226 test_sampler io_probe{100ms, get_io_context()};
227 io_probe.probe_.cancel_async();
228 except<std::logic_error>([&io_probe]() { io_probe.start_one(); });
229 except<std::logic_error>([&io_probe]() { io_probe.start(); });
230 }
231
232public:
233 void
234 run() override
235 {
236 yield_to([&](boost::asio::yield_context& yield) {
237 testSampleOne(yield);
238 testSampleOngoing(yield);
239 testCanceled(yield);
240 });
241 }
242};
243
244BEAST_DEFINE_TESTSUITE(io_latency_probe, beast, beast);
T begin(T... args)
Measures handler latency on an io_context queue.
void sample(Handler &&handler)
Initiate continuous i/o latency sampling.
void sample_one(Handler &&handler)
Measure one sample of i/o latency.
Mix-in to support tests using asio coroutines.
Definition yield_to.h:32
boost::asio::io_context & get_io_context()
Return the io_context associated with the object.
Definition yield_to.h:66
void yield_to(F0 &&f0, FN &&... fn)
Run one or more functions, each in a coroutine.
Definition yield_to.h:105
A testsuite class.
Definition suite.h:55
log_os< char > log
Logging output stream.
Definition suite.h:152
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:155
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 emplace_back(T... args)
T end(T... args)
T max_element(T... args)
T min_element(T... args)
T push_back(T... args)
T ref(T... args)
T reserve(T... args)
T size(T... args)
void operator()(std::chrono::steady_clock::duration const &elapsed)
beast::io_latency_probe< std::chrono::steady_clock > probe_
std::vector< std::chrono::steady_clock::duration > durations_
test_sampler(std::chrono::milliseconds interval, boost::asio::io_context &ios)
T to_string(T... args)