agent-enviroments/builder/libs/seastar/tests/perf/perf_tests.cc
2024-09-10 17:06:08 +03:00

488 lines
16 KiB
C++

/*
* This file is open source software, licensed to you under the terms
* of the Apache License, Version 2.0 (the "License"). See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. You may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*
* Copyright (C) 2018 ScyllaDB Ltd.
*/
#include <seastar/testing/perf_tests.hh>
#include <cstdio>
#include <fstream>
#include <regex>
#include <boost/range.hpp>
#include <boost/range/adaptors.hpp>
#include <boost/range/algorithm.hpp>
#include <fmt/ostream.h>
#include <seastar/core/app-template.hh>
#include <seastar/core/thread.hh>
#include <seastar/core/sharded.hh>
#include <seastar/json/formatter.hh>
#include <seastar/util/later.hh>
#include <seastar/testing/random.hh>
#include <seastar/core/memory.hh>
#include <seastar/core/reactor.hh>
#include <signal.h>
#if FMT_VERSION >= 90000
namespace perf_tests::internal {
struct duration;
}
template <> struct fmt::formatter<perf_tests::internal::duration> : fmt::ostream_formatter {};
#endif
namespace perf_tests {
namespace internal {
namespace {
// We need to use signal-based timer instead of seastar ones so that
// tests that do not suspend can be interrupted.
// This causes no overhead though since the timer is used only in a dry run.
class signal_timer {
std::function<void()> _fn;
timer_t _timer;
public:
explicit signal_timer(std::function<void()> fn) : _fn(fn) {
sigevent se{};
se.sigev_notify = SIGEV_SIGNAL;
se.sigev_signo = SIGALRM;
se.sigev_value.sival_ptr = this;
auto ret = timer_create(CLOCK_MONOTONIC, &se, &_timer);
if (ret) {
throw std::system_error(ret, std::system_category());
}
}
~signal_timer() {
timer_delete(_timer);
}
void arm(std::chrono::steady_clock::duration dt) {
time_t sec = std::chrono::duration_cast<std::chrono::seconds>(dt).count();
auto nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(dt).count();
nsec -= std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(sec)).count();
itimerspec ts{};
ts.it_value.tv_sec = sec;
ts.it_value.tv_nsec = nsec;
auto ret = timer_settime(_timer, 0, &ts, nullptr);
if (ret) {
throw std::system_error(ret, std::system_category());
}
}
void cancel() {
itimerspec ts{};
auto ret = timer_settime(_timer, 0, &ts, nullptr);
if (ret) {
throw std::system_error(ret, std::system_category());
}
}
public:
static void init() {
struct sigaction sa{};
sa.sa_sigaction = &signal_timer::signal_handler;
sa.sa_flags = SA_SIGINFO;
auto ret = sigaction(SIGALRM, &sa, nullptr);
if (ret) {
throw std::system_error(ret, std::system_category());
}
}
private:
static void signal_handler(int, siginfo_t* si, void*) {
auto t = static_cast<signal_timer*>(si->si_value.sival_ptr);
t->_fn();
}
};
}
uint64_t perf_stats::perf_mallocs() {
return memory::stats().mallocs();
}
uint64_t perf_stats::perf_tasks_processed() {
return engine().get_sched_stats().tasks_processed;
}
perf_stats perf_stats::snapshot(linux_perf_event* instructions_retired_counter, linux_perf_event* cpu_cycles_retired_counter) {
return perf_stats(
perf_mallocs(),
perf_tasks_processed(),
instructions_retired_counter ? instructions_retired_counter->read() : 0,
cpu_cycles_retired_counter ? cpu_cycles_retired_counter->read() : 0
);
}
time_measurement measure_time;
struct config;
struct result;
struct result_printer {
virtual ~result_printer() = default;
virtual void print_configuration(const config&) = 0;
virtual void print_result(const result&) = 0;
void update_name_column_length(size_t length) {
_name_column_length = std::max(1ul, length);
}
size_t name_column_length() const {
return _name_column_length;
}
private:
static constexpr size_t DEFAULT_NAME_COLUMN_LENGTH = 40;
size_t _name_column_length = DEFAULT_NAME_COLUMN_LENGTH;
};
struct config {
uint64_t single_run_iterations;
std::chrono::nanoseconds single_run_duration;
unsigned number_of_runs;
std::vector<std::unique_ptr<result_printer>> printers;
unsigned random_seed = 0;
};
struct result {
sstring test_name = "";
uint64_t total_iterations = 0;
unsigned runs = 0;
double median = 0.;
double mad = 0.;
double min = 0.;
double max = 0.;
double allocs = 0.;
double tasks = 0.;
double inst = 0.;
double cycles = 0.;
};
struct duration {
double value;
};
static inline std::ostream& operator<<(std::ostream& os, duration d)
{
auto value = d.value;
if (value < 1'000) {
os << fmt::format("{:.3f}ns", value);
} else if (value < 1'000'000) {
// fmt hasn't discovered unicode yet so we are stuck with uicroseconds
// See: https://github.com/fmtlib/fmt/issues/628
os << fmt::format("{:.3f}us", value / 1'000);
} else if (value < 1'000'000'000) {
os << fmt::format("{:.3f}ms", value / 1'000'000);
} else {
os << fmt::format("{:.3f}s", value / 1'000'000'000);
}
return os;
}
struct stdout_printer final : result_printer {
virtual void print_configuration(const config& c) override {
fmt::print("{:<25} {}\n{:<25} {}\n{:<25} {}\n{:<25} {}\n{:<25} {}\n\n",
"single run iterations:", c.single_run_iterations,
"single run duration:", duration { double(c.single_run_duration.count()) },
"number of runs:", c.number_of_runs,
"number of cores:", smp::count,
"random seed:", c.random_seed);
fmt::print(header_format_string, "test", name_column_length(), "iterations", "median", "mad", "min", "max", "allocs", "tasks", "inst", "cycles");
}
virtual void print_result(const result& r) override {
fmt::print(format_string, r.test_name, name_column_length(), r.total_iterations / r.runs, duration { r.median },
duration { r.mad }, duration { r.min }, duration { r.max },
r.allocs, r.tasks, r.inst, r.cycles);
}
private:
static constexpr auto header_format_string ="{:<{}} {:>11} {:>11} {:>11} {:>11} {:>11} {:>11} {:>11} {:>11} {:>11}\n";
static constexpr auto format_string = "{:<{}} {:>11} {:>11} {:>11} {:>11} {:>11} {:>11.3f} {:>11.3f} {:>11.1f} {:>11.1f}\n";
};
class json_printer final : public result_printer {
std::string _output_file;
std::unordered_map<std::string,
std::unordered_map<std::string,
std::unordered_map<std::string, double>>> _root;
public:
explicit json_printer(const std::string& file) : _output_file(file) { }
~json_printer() {
std::ofstream out(_output_file);
out << json::formatter::to_json(_root);
}
virtual void print_configuration(const config&) override { }
virtual void print_result(const result& r) override {
auto& result = _root["results"][r.test_name];
result["runs"] = r.runs;
result["total_iterations"] = r.total_iterations;
result["median"] = r.median;
result["mad"] = r.mad;
result["min"] = r.min;
result["max"] = r.max;
result["allocs"] = r.allocs;
result["tasks"] = r.tasks;
result["inst"] = r.inst;
result["cycles"] = r.cycles;
}
};
class markdown_printer final : public result_printer {
static constexpr std::string_view header_format_string = "| {:<{}} | {:>11} | {:>11} | {:>11} | {:>11} | {:>11} | {:>11} | {:>11} | {:>11} |\n";
static constexpr std::string_view body_format_string = "| {:<{}} | {:>11} | {:>11} | {:>11} | {:>11} | {:>11} | {:>11.3f} | {:>11.3f} | {:>11.1f} |\n";
std::FILE* _output = nullptr;
public:
explicit markdown_printer(const std::string& filename) {
if (filename == "-") {
_output = stdout;
} else {
_output = std::fopen(filename.c_str(), "w");
}
if (!_output) {
throw std::invalid_argument(fmt::format("unable to write to {}", filename));
}
}
~markdown_printer() {
if (_output != stdout) {
std::fclose(_output);
}
}
void print_configuration(const config&) override {
fmt::print(_output, header_format_string, "test", name_column_length(), "iterations", "median", "mad", "min", "max", "allocs", "tasks", "inst");
fmt::print(_output, header_format_string, "-", name_column_length(), "-", "-", "-", "-", "-", "-", "-", "-");
}
void print_result(const result& r) override {
fmt::print(_output, body_format_string, r.test_name, name_column_length(), r.total_iterations / r.runs, duration { r.median },
duration { r.mad }, duration { r.min }, duration { r.max },
r.allocs, r.tasks, r.inst);
}
};
void performance_test::do_run(const config& conf)
{
_max_single_run_iterations = conf.single_run_iterations;
if (!_max_single_run_iterations) {
_max_single_run_iterations = std::numeric_limits<uint64_t>::max();
}
signal_timer tmr([this] {
_max_single_run_iterations.store(0, std::memory_order_relaxed);
});
// dry run, estimate the number of iterations
if (conf.single_run_duration.count()) {
// switch out of seastar thread
yield().then([&] {
tmr.arm(conf.single_run_duration);
return do_single_run().finally([&] {
tmr.cancel();
_max_single_run_iterations = _single_run_iterations;
});
}).get();
}
result r{};
auto results = std::vector<double>(conf.number_of_runs);
uint64_t total_iterations = 0;
for (auto i = 0u; i < conf.number_of_runs; i++) {
// switch out of seastar thread
yield().then([&] {
_single_run_iterations = 0;
return do_single_run().then([&] (run_result rr) {
clock_type::duration dt = rr.duration;
double ns = std::chrono::duration_cast<std::chrono::nanoseconds>(dt).count();
results[i] = ns / _single_run_iterations;
total_iterations += _single_run_iterations;
r.allocs += double(rr.stats.allocations) / _single_run_iterations;
r.tasks += double(rr.stats.tasks_executed) / _single_run_iterations;
r.inst += double(rr.stats.instructions_retired) / _single_run_iterations;
r.cycles += double(rr.stats.cpu_cycles_retired) / _single_run_iterations;
});
}).get();
}
r.test_name = name();
r.total_iterations = total_iterations;
r.runs = conf.number_of_runs;
auto mid = conf.number_of_runs / 2;
boost::range::sort(results);
r.median = results[mid];
auto diffs = boost::copy_range<std::vector<double>>(
results | boost::adaptors::transformed([&] (double x) { return fabs(x - r.median); })
);
boost::range::sort(diffs);
r.mad = diffs[mid];
r.min = results[0];
r.max = results[results.size() - 1];
r.allocs /= conf.number_of_runs;
r.tasks /= conf.number_of_runs;
r.inst /= conf.number_of_runs;
r.cycles /= conf.number_of_runs;
for (auto& rp : conf.printers) {
rp->print_result(r);
}
}
void performance_test::run(const config& conf)
{
set_up();
try {
do_run(conf);
} catch (...) {
tear_down();
throw;
}
tear_down();
}
std::vector<std::unique_ptr<performance_test>>& all_tests()
{
static std::vector<std::unique_ptr<performance_test>> tests;
return tests;
}
void performance_test::register_test(std::unique_ptr<performance_test> test)
{
all_tests().emplace_back(std::move(test));
}
void run_all(const std::vector<std::string>& tests, config& conf)
{
auto can_run = [tests = boost::copy_range<std::vector<std::regex>>(tests)] (auto&& test) {
auto it = boost::range::find_if(tests, [&test] (const std::regex& regex) {
return std::regex_match(test->name(), regex);
});
return tests.empty() || it != tests.end();
};
size_t max_name_column_length = 0;
for (auto&& test : all_tests() | boost::adaptors::filtered(can_run)) {
max_name_column_length = std::max(max_name_column_length, test->name().size());
}
for (auto& rp : conf.printers) {
rp->update_name_column_length(max_name_column_length);
rp->print_configuration(conf);
}
for (auto&& test : all_tests() | boost::adaptors::filtered(std::move(can_run))) {
test->run(conf);
}
}
}
}
int main(int ac, char** av)
{
using namespace perf_tests::internal;
namespace bpo = boost::program_options;
app_template app;
app.add_options()
("iterations,i", bpo::value<size_t>()->default_value(0),
"number of iterations in a single run")
("duration,d", bpo::value<double>()->default_value(1),
"duration of a single run in seconds")
("runs,r", bpo::value<size_t>()->default_value(5), "number of runs")
("test,t", bpo::value<std::vector<std::string>>(), "tests to execute")
("random-seed,S", bpo::value<unsigned>()->default_value(0),
"random number generator seed")
("no-stdout", "do not print to stdout")
("json-output", bpo::value<std::string>(), "output json file")
("md-output", bpo::value<std::string>(), "output markdown file")
("list", "list available tests")
;
return app.run(ac, av, [&] {
return async([&] {
signal_timer::init();
config conf;
conf.single_run_iterations = app.configuration()["iterations"].as<size_t>();
auto dur = std::chrono::duration<double>(app.configuration()["duration"].as<double>());
conf.single_run_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(dur);
conf.number_of_runs = app.configuration()["runs"].as<size_t>();
conf.random_seed = app.configuration()["random-seed"].as<unsigned>();
std::vector<std::string> tests_to_run;
if (app.configuration().count("test")) {
tests_to_run = app.configuration()["test"].as<std::vector<std::string>>();
}
if (app.configuration().count("list")) {
fmt::print("available tests:\n");
for (auto&& t : all_tests()) {
fmt::print("\t{}\n", t->name());
}
return;
}
if (!app.configuration().count("no-stdout")) {
conf.printers.emplace_back(std::make_unique<stdout_printer>());
}
if (app.configuration().count("json-output")) {
conf.printers.emplace_back(std::make_unique<json_printer>(
app.configuration()["json-output"].as<std::string>()
));
}
if (app.configuration().count("md-output")) {
conf.printers.emplace_back(std::make_unique<markdown_printer>(
app.configuration()["md-output"].as<std::string>()
));
}
if (!conf.random_seed) {
conf.random_seed = std::random_device()();
}
smp::invoke_on_all([seed = conf.random_seed] {
auto local_seed = seed + this_shard_id();
testing::local_random_engine.seed(local_seed);
}).get();
run_all(tests_to_run, conf);
});
});
}