2 * This file is open source software, licensed to you under the terms
3 * of the Apache License, Version 2.0 (the "License"). See the NOTICE file
4 * distributed with this work for additional information regarding copyright
5 * ownership. You may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing,
12 * software distributed under the License is distributed on an
13 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 * KIND, either express or implied. See the License for the
15 * specific language governing permissions and limitations
19 * Copyright (C) 2018 ScyllaDB Ltd.
22 #include <seastar/testing/perf_tests.hh>
27 #include <boost/range.hpp>
28 #include <boost/range/adaptors.hpp>
29 #include <boost/range/algorithm.hpp>
31 #include <fmt/ostream.h>
33 #include <seastar/core/app-template.hh>
34 #include <seastar/core/thread.hh>
35 #include <seastar/json/formatter.hh>
37 namespace perf_tests
{
42 // We need to use signal-based timer instead of seastar ones so that
43 // tests that do not suspend can be interrupted.
44 // This causes no overhead though since the timer is used only in a dry run.
46 std::function
<void()> _fn
;
49 explicit signal_timer(std::function
<void()> fn
) : _fn(fn
) {
51 se
.sigev_notify
= SIGEV_SIGNAL
;
52 se
.sigev_signo
= SIGALRM
;
53 se
.sigev_value
.sival_ptr
= this;
54 auto ret
= timer_create(CLOCK_MONOTONIC
, &se
, &_timer
);
56 throw std::system_error(ret
, std::system_category());
64 void arm(std::chrono::steady_clock::duration dt
) {
65 time_t sec
= std::chrono::duration_cast
<std::chrono::seconds
>(dt
).count();
66 auto nsec
= std::chrono::duration_cast
<std::chrono::nanoseconds
>(dt
).count();
67 nsec
-= std::chrono::duration_cast
<std::chrono::nanoseconds
>(std::chrono::seconds(sec
)).count();
70 ts
.it_value
.tv_sec
= sec
;
71 ts
.it_value
.tv_nsec
= nsec
;
72 auto ret
= timer_settime(_timer
, 0, &ts
, nullptr);
74 throw std::system_error(ret
, std::system_category());
80 auto ret
= timer_settime(_timer
, 0, &ts
, nullptr);
82 throw std::system_error(ret
, std::system_category());
87 struct sigaction sa
{};
88 sa
.sa_sigaction
= &signal_timer::signal_handler
;
89 sa
.sa_flags
= SA_SIGINFO
;
90 auto ret
= sigaction(SIGALRM
, &sa
, nullptr);
92 throw std::system_error(ret
, std::system_category());
96 static void signal_handler(int, siginfo_t
* si
, void*) {
97 auto t
= static_cast<signal_timer
*>(si
->si_value
.sival_ptr
);
104 time_measurement measure_time
;
109 struct result_printer
{
110 virtual ~result_printer() = default;
112 virtual void print_configuration(const config
&) = 0;
113 virtual void print_result(const result
&) = 0;
117 uint64_t single_run_iterations
;
118 std::chrono::nanoseconds single_run_duration
;
119 unsigned number_of_runs
;
120 std::vector
<std::unique_ptr
<result_printer
>> printers
;
126 uint64_t total_iterations
;
141 static inline std::ostream
& operator<<(std::ostream
& os
, duration d
)
143 auto value
= d
.value
;
145 os
<< fmt::format("{:.3f}ns", value
);
146 } else if (value
< 1'000'000) {
147 // fmt hasn't discovered unicode yet so we are stuck with uicroseconds
148 // See: https://github.com/fmtlib/fmt/issues/628
149 os
<< fmt::format("{:.3f}us", value
/ 1'000);
150 } else if (value
< 1'000'000'000) {
151 os
<< fmt::format("{:.3f}ms", value
/ 1'000'000);
153 os
<< fmt::format("{:.3f}s", value
/ 1'000'000'000);
160 static constexpr auto format_string
= "{:<40} {:>11} {:>11} {:>11} {:>11} {:>11}\n";
162 struct stdout_printer final
: result_printer
{
163 virtual void print_configuration(const config
& c
) override
{
164 fmt::print("{:<25} {}\n{:<25} {}\n{:<25} {}\n\n",
165 "single run iterations:", c
.single_run_iterations
,
166 "single run duration:", duration
{ double(c
.single_run_duration
.count()) },
167 "number of runs:", c
.number_of_runs
);
168 fmt::print(format_string
, "test", "iterations", "median", "mad", "min", "max");
171 virtual void print_result(const result
& r
) override
{
172 fmt::print(format_string
, r
.test_name
, r
.total_iterations
/ r
.runs
, duration
{ r
.median
},
173 duration
{ r
.mad
}, duration
{ r
.min
}, duration
{ r
.max
});
177 class json_printer final
: public result_printer
{
178 std::string _output_file
;
179 std::unordered_map
<std::string
,
180 std::unordered_map
<std::string
,
181 std::unordered_map
<std::string
, double>>> _root
;
183 explicit json_printer(const std::string
& file
) : _output_file(file
) { }
186 std::ofstream
out(_output_file
);
187 out
<< json::formatter::to_json(_root
);
190 virtual void print_configuration(const config
&) override
{ }
192 virtual void print_result(const result
& r
) override
{
193 auto& result
= _root
["results"][r
.test_name
];
194 result
["runs"] = r
.runs
;
195 result
["total_iterations"] = r
.total_iterations
;
196 result
["median"] = r
.median
;
197 result
["mad"] = r
.mad
;
198 result
["min"] = r
.min
;
199 result
["max"] = r
.max
;
203 void performance_test::do_run(const config
& conf
)
205 _max_single_run_iterations
= conf
.single_run_iterations
;
206 if (!_max_single_run_iterations
) {
207 _max_single_run_iterations
= std::numeric_limits
<uint64_t>::max();
210 signal_timer
tmr([this] {
211 _max_single_run_iterations
.store(0, std::memory_order_relaxed
);
214 // dry run, estimate the number of iterations
215 if (conf
.single_run_duration
.count()) {
216 // switch out of seastar thread
218 tmr
.arm(conf
.single_run_duration
);
219 return do_single_run().finally([&] {
221 _max_single_run_iterations
= _single_run_iterations
;
226 auto results
= std::vector
<double>(conf
.number_of_runs
);
227 uint64_t total_iterations
= 0;
228 for (auto i
= 0u; i
< conf
.number_of_runs
; i
++) {
229 // switch out of seastar thread
231 _single_run_iterations
= 0;
232 return do_single_run().then([&] (clock_type::duration dt
) {
233 double ns
= std::chrono::duration_cast
<std::chrono::nanoseconds
>(dt
).count();
234 results
[i
] = ns
/ _single_run_iterations
;
236 total_iterations
+= _single_run_iterations
;
242 r
.test_name
= name();
243 r
.total_iterations
= total_iterations
;
244 r
.runs
= conf
.number_of_runs
;
246 auto mid
= conf
.number_of_runs
/ 2;
248 boost::range::sort(results
);
249 r
.median
= results
[mid
];
251 auto diffs
= boost::copy_range
<std::vector
<double>>(
252 results
| boost::adaptors::transformed([&] (double x
) { return fabs(x
- r
.median
); })
254 boost::range::sort(diffs
);
258 r
.max
= results
[results
.size() - 1];
260 for (auto& rp
: conf
.printers
) {
265 void performance_test::run(const config
& conf
)
277 std::vector
<std::unique_ptr
<performance_test
>>& all_tests()
279 static std::vector
<std::unique_ptr
<performance_test
>> tests
;
283 void performance_test::register_test(std::unique_ptr
<performance_test
> test
)
285 all_tests().emplace_back(std::move(test
));
288 void run_all(const std::vector
<std::string
>& tests
, const config
& conf
)
290 auto can_run
= [tests
= boost::copy_range
<std::vector
<std::regex
>>(tests
)] (auto&& test
) {
291 auto it
= boost::range::find_if(tests
, [&test
] (const std::regex
& regex
) {
292 return std::regex_match(test
->name(), regex
);
294 return tests
.empty() || it
!= tests
.end();
297 for (auto& rp
: conf
.printers
) {
298 rp
->print_configuration(conf
);
300 for (auto&& test
: all_tests() | boost::adaptors::filtered(std::move(can_run
))) {
308 int main(int ac
, char** av
)
310 using namespace perf_tests::internal
;
311 namespace bpo
= boost::program_options
;
315 ("iterations,i", bpo::value
<size_t>()->default_value(0),
316 "number of iterations in a single run")
317 ("duration,d", bpo::value
<double>()->default_value(1),
318 "duration of a single run in seconds")
319 ("runs,r", bpo::value
<size_t>()->default_value(5), "number of runs")
320 ("test,t", bpo::value
<std::vector
<std::string
>>(), "tests to execute")
321 ("no-stdout", "do not print to stdout")
322 ("json-output", bpo::value
<std::string
>(), "output json file")
323 ("list", "list available tests")
326 return app
.run(ac
, av
, [&] {
328 signal_timer::init();
331 conf
.single_run_iterations
= app
.configuration()["iterations"].as
<size_t>();
332 auto dur
= std::chrono::duration
<double>(app
.configuration()["duration"].as
<double>());
333 conf
.single_run_duration
= std::chrono::duration_cast
<std::chrono::nanoseconds
>(dur
);
334 conf
.number_of_runs
= app
.configuration()["runs"].as
<size_t>();
336 std::vector
<std::string
> tests_to_run
;
337 if (app
.configuration().count("test")) {
338 tests_to_run
= app
.configuration()["test"].as
<std::vector
<std::string
>>();
341 if (app
.configuration().count("list")) {
342 fmt::print("available tests:\n");
343 for (auto&& t
: all_tests()) {
344 fmt::print("\t{}\n", t
->name());
349 if (!app
.configuration().count("no-stdout")) {
350 conf
.printers
.emplace_back(std::make_unique
<stdout_printer
>());
353 if (app
.configuration().count("json-output")) {
354 conf
.printers
.emplace_back(std::make_unique
<json_printer
>(
355 app
.configuration()["json-output"].as
<std::string
>()
359 run_all(tests_to_run
, conf
);