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>
36 #include <seastar/util/later.hh>
40 namespace perf_tests
{
45 // We need to use signal-based timer instead of seastar ones so that
46 // tests that do not suspend can be interrupted.
47 // This causes no overhead though since the timer is used only in a dry run.
49 std::function
<void()> _fn
;
52 explicit signal_timer(std::function
<void()> fn
) : _fn(fn
) {
54 se
.sigev_notify
= SIGEV_SIGNAL
;
55 se
.sigev_signo
= SIGALRM
;
56 se
.sigev_value
.sival_ptr
= this;
57 auto ret
= timer_create(CLOCK_MONOTONIC
, &se
, &_timer
);
59 throw std::system_error(ret
, std::system_category());
67 void arm(std::chrono::steady_clock::duration dt
) {
68 time_t sec
= std::chrono::duration_cast
<std::chrono::seconds
>(dt
).count();
69 auto nsec
= std::chrono::duration_cast
<std::chrono::nanoseconds
>(dt
).count();
70 nsec
-= std::chrono::duration_cast
<std::chrono::nanoseconds
>(std::chrono::seconds(sec
)).count();
73 ts
.it_value
.tv_sec
= sec
;
74 ts
.it_value
.tv_nsec
= nsec
;
75 auto ret
= timer_settime(_timer
, 0, &ts
, nullptr);
77 throw std::system_error(ret
, std::system_category());
83 auto ret
= timer_settime(_timer
, 0, &ts
, nullptr);
85 throw std::system_error(ret
, std::system_category());
90 struct sigaction sa
{};
91 sa
.sa_sigaction
= &signal_timer::signal_handler
;
92 sa
.sa_flags
= SA_SIGINFO
;
93 auto ret
= sigaction(SIGALRM
, &sa
, nullptr);
95 throw std::system_error(ret
, std::system_category());
99 static void signal_handler(int, siginfo_t
* si
, void*) {
100 auto t
= static_cast<signal_timer
*>(si
->si_value
.sival_ptr
);
107 time_measurement measure_time
;
112 struct result_printer
{
113 virtual ~result_printer() = default;
115 virtual void print_configuration(const config
&) = 0;
116 virtual void print_result(const result
&) = 0;
120 uint64_t single_run_iterations
;
121 std::chrono::nanoseconds single_run_duration
;
122 unsigned number_of_runs
;
123 std::vector
<std::unique_ptr
<result_printer
>> printers
;
129 uint64_t total_iterations
;
144 static inline std::ostream
& operator<<(std::ostream
& os
, duration d
)
146 auto value
= d
.value
;
148 os
<< fmt::format("{:.3f}ns", value
);
149 } else if (value
< 1'000'000) {
150 // fmt hasn't discovered unicode yet so we are stuck with uicroseconds
151 // See: https://github.com/fmtlib/fmt/issues/628
152 os
<< fmt::format("{:.3f}us", value
/ 1'000);
153 } else if (value
< 1'000'000'000) {
154 os
<< fmt::format("{:.3f}ms", value
/ 1'000'000);
156 os
<< fmt::format("{:.3f}s", value
/ 1'000'000'000);
163 static constexpr auto format_string
= "{:<40} {:>11} {:>11} {:>11} {:>11} {:>11}\n";
165 struct stdout_printer final
: result_printer
{
166 virtual void print_configuration(const config
& c
) override
{
167 fmt::print("{:<25} {}\n{:<25} {}\n{:<25} {}\n\n",
168 "single run iterations:", c
.single_run_iterations
,
169 "single run duration:", duration
{ double(c
.single_run_duration
.count()) },
170 "number of runs:", c
.number_of_runs
);
171 fmt::print(format_string
, "test", "iterations", "median", "mad", "min", "max");
174 virtual void print_result(const result
& r
) override
{
175 fmt::print(format_string
, r
.test_name
, r
.total_iterations
/ r
.runs
, duration
{ r
.median
},
176 duration
{ r
.mad
}, duration
{ r
.min
}, duration
{ r
.max
});
180 class json_printer final
: public result_printer
{
181 std::string _output_file
;
182 std::unordered_map
<std::string
,
183 std::unordered_map
<std::string
,
184 std::unordered_map
<std::string
, double>>> _root
;
186 explicit json_printer(const std::string
& file
) : _output_file(file
) { }
189 std::ofstream
out(_output_file
);
190 out
<< json::formatter::to_json(_root
);
193 virtual void print_configuration(const config
&) override
{ }
195 virtual void print_result(const result
& r
) override
{
196 auto& result
= _root
["results"][r
.test_name
];
197 result
["runs"] = r
.runs
;
198 result
["total_iterations"] = r
.total_iterations
;
199 result
["median"] = r
.median
;
200 result
["mad"] = r
.mad
;
201 result
["min"] = r
.min
;
202 result
["max"] = r
.max
;
206 void performance_test::do_run(const config
& conf
)
208 _max_single_run_iterations
= conf
.single_run_iterations
;
209 if (!_max_single_run_iterations
) {
210 _max_single_run_iterations
= std::numeric_limits
<uint64_t>::max();
213 signal_timer
tmr([this] {
214 _max_single_run_iterations
.store(0, std::memory_order_relaxed
);
217 // dry run, estimate the number of iterations
218 if (conf
.single_run_duration
.count()) {
219 // switch out of seastar thread
221 tmr
.arm(conf
.single_run_duration
);
222 return do_single_run().finally([&] {
224 _max_single_run_iterations
= _single_run_iterations
;
229 auto results
= std::vector
<double>(conf
.number_of_runs
);
230 uint64_t total_iterations
= 0;
231 for (auto i
= 0u; i
< conf
.number_of_runs
; i
++) {
232 // switch out of seastar thread
234 _single_run_iterations
= 0;
235 return do_single_run().then([&] (clock_type::duration dt
) {
236 double ns
= std::chrono::duration_cast
<std::chrono::nanoseconds
>(dt
).count();
237 results
[i
] = ns
/ _single_run_iterations
;
239 total_iterations
+= _single_run_iterations
;
245 r
.test_name
= name();
246 r
.total_iterations
= total_iterations
;
247 r
.runs
= conf
.number_of_runs
;
249 auto mid
= conf
.number_of_runs
/ 2;
251 boost::range::sort(results
);
252 r
.median
= results
[mid
];
254 auto diffs
= boost::copy_range
<std::vector
<double>>(
255 results
| boost::adaptors::transformed([&] (double x
) { return fabs(x
- r
.median
); })
257 boost::range::sort(diffs
);
261 r
.max
= results
[results
.size() - 1];
263 for (auto& rp
: conf
.printers
) {
268 void performance_test::run(const config
& conf
)
280 std::vector
<std::unique_ptr
<performance_test
>>& all_tests()
282 static std::vector
<std::unique_ptr
<performance_test
>> tests
;
286 void performance_test::register_test(std::unique_ptr
<performance_test
> test
)
288 all_tests().emplace_back(std::move(test
));
291 void run_all(const std::vector
<std::string
>& tests
, const config
& conf
)
293 auto can_run
= [tests
= boost::copy_range
<std::vector
<std::regex
>>(tests
)] (auto&& test
) {
294 auto it
= boost::range::find_if(tests
, [&test
] (const std::regex
& regex
) {
295 return std::regex_match(test
->name(), regex
);
297 return tests
.empty() || it
!= tests
.end();
300 for (auto& rp
: conf
.printers
) {
301 rp
->print_configuration(conf
);
303 for (auto&& test
: all_tests() | boost::adaptors::filtered(std::move(can_run
))) {
311 int main(int ac
, char** av
)
313 using namespace perf_tests::internal
;
314 namespace bpo
= boost::program_options
;
318 ("iterations,i", bpo::value
<size_t>()->default_value(0),
319 "number of iterations in a single run")
320 ("duration,d", bpo::value
<double>()->default_value(1),
321 "duration of a single run in seconds")
322 ("runs,r", bpo::value
<size_t>()->default_value(5), "number of runs")
323 ("test,t", bpo::value
<std::vector
<std::string
>>(), "tests to execute")
324 ("no-stdout", "do not print to stdout")
325 ("json-output", bpo::value
<std::string
>(), "output json file")
326 ("list", "list available tests")
329 return app
.run(ac
, av
, [&] {
331 signal_timer::init();
334 conf
.single_run_iterations
= app
.configuration()["iterations"].as
<size_t>();
335 auto dur
= std::chrono::duration
<double>(app
.configuration()["duration"].as
<double>());
336 conf
.single_run_duration
= std::chrono::duration_cast
<std::chrono::nanoseconds
>(dur
);
337 conf
.number_of_runs
= app
.configuration()["runs"].as
<size_t>();
339 std::vector
<std::string
> tests_to_run
;
340 if (app
.configuration().count("test")) {
341 tests_to_run
= app
.configuration()["test"].as
<std::vector
<std::string
>>();
344 if (app
.configuration().count("list")) {
345 fmt::print("available tests:\n");
346 for (auto&& t
: all_tests()) {
347 fmt::print("\t{}\n", t
->name());
352 if (!app
.configuration().count("no-stdout")) {
353 conf
.printers
.emplace_back(std::make_unique
<stdout_printer
>());
356 if (app
.configuration().count("json-output")) {
357 conf
.printers
.emplace_back(std::make_unique
<json_printer
>(
358 app
.configuration()["json-output"].as
<std::string
>()
362 run_all(tests_to_run
, conf
);