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

250 lines
8.3 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) 2022 ScyllaDB
*/
#include <random>
#include <boost/range/irange.hpp>
#include <fmt/core.h>
#include <seastar/core/app-template.hh>
#include <seastar/core/thread.hh>
#include <seastar/core/do_with.hh>
#include <seastar/core/loop.hh>
#include <seastar/core/sleep.hh>
#include <seastar/util/later.hh>
using namespace seastar;
using namespace std::chrono;
class thinker {
class poisson_process {
std::random_device _rd;
std::mt19937 _rng;
std::exponential_distribution<double> _exp;
public:
poisson_process(duration<double> period)
: _rng(_rd())
, _exp(1.0 / period.count())
{
}
duration<double> get() {
return duration<double>(_exp(_rng));
}
};
poisson_process _pause;
bool _stop;
future<> _done;
future<> start_thinking(unsigned concurrency) {
return parallel_for_each(boost::irange(0u, concurrency), [this] (unsigned f) {
return do_until([this] { return _stop; }, [this] {
auto until = steady_clock::now() + _pause.get();
while (steady_clock::now() < until) {
; // do nothing
}
return make_ready_future<>();
});
});
}
public:
thinker(unsigned concurrency, microseconds think) noexcept
: _pause(think)
, _stop(false)
, _done(start_thinking(concurrency))
{
fmt::print("shard {} starts {}x{}us thinkers\n", this_shard_id(), concurrency, think.count());
}
future<> stop() {
_stop = true;
return std::move(_done);
}
};
enum class respond_type { ready, yield, io, timer };
static respond_type parse_respond_type(std::string s) {
if (s == "ready") {
return respond_type::ready;
}
if (s == "yield") {
return respond_type::yield;
}
if (s == "io") {
return respond_type::io;
}
if (s == "timer") {
return respond_type::timer;
}
throw std::runtime_error("unknown respond type");
}
class worker {
const unsigned _to;
std::unique_ptr<thinker> _think;
uint64_t _total;
bool _stop;
future<> _done;
static unsigned my_target(unsigned targets) noexcept {
unsigned group_size = (smp::count + (targets - 1)) / targets;
unsigned group_no = this_shard_id() / group_size;
return group_size * group_no;
}
future<> start_working(unsigned concurrency, respond_type resp, microseconds tmo) {
return parallel_for_each(boost::irange(0u, concurrency), [this, resp, tmo] (unsigned f) {
return do_until([this] { return _stop; }, [this, resp, tmo] {
return smp::submit_to(_to, [resp, tmo] {
switch (resp) {
case respond_type::ready:
return make_ready_future<>();
case respond_type::yield:
return yield();
case respond_type::io:
return check_for_io_immediately();
case respond_type::timer:
return seastar::sleep<lowres_clock>(tmo);
}
__builtin_unreachable();
}).then([this] {
_total++;
return make_ready_future<>();
});
});
});
}
public:
struct config {
unsigned targets;
unsigned thinkers;
microseconds think;
respond_type respond;
microseconds respond_tmo;
unsigned concurrency;
};
worker(config cfg) noexcept
: _to(my_target(cfg.targets))
, _think(is_target() && (cfg.thinkers > 0) ? std::make_unique<thinker>(cfg.thinkers, cfg.think) : nullptr)
, _total(0)
, _stop(false)
, _done(start_working(cfg.concurrency, cfg.respond, cfg.respond_tmo))
{
}
future<> stop() {
if (_stop) {
return make_ready_future<>();
}
_stop = true;
return std::move(_done).then([this] {
return _think ? _think->stop() : make_ready_future<>();
});
}
bool is_target() const noexcept { return _to == this_shard_id(); }
uint64_t total() const noexcept { return _total; }
};
class stats {
uint64_t _min = std::numeric_limits<uint64_t>::max();
uint64_t _max = std::numeric_limits<uint64_t>::min();
uint64_t _sum = 0;
unsigned _nr = 0;
const unsigned _norm;
public:
stats(unsigned norm) noexcept : _norm(norm) {}
void append(uint64_t val) noexcept {
_min = std::min(_min, val);
_max = std::max(_max, val);
_sum += val;
_nr++;
}
double min() const noexcept { return (double)_min / _norm; }
double max() const noexcept { return (double)_max / _norm; }
double avg() const noexcept { return _nr > 0 ? (double)_sum / _nr / _norm : 0.0; }
unsigned nr() const noexcept { return _nr; }
};
int main(int ac, char** av) {
app_template at;
namespace bpo = boost::program_options;
at.add_options()
("duration", bpo::value<unsigned>()->default_value(32), "time to run the test (seconds)")
("targets", bpo::value<unsigned>()->default_value(1), "number of responder shards")
("thinkers", bpo::value<unsigned>()->default_value(0), "thinker fibers to run in parallel on targets")
("think", bpo::value<unsigned>()->default_value(100), "time (us) thinkers busyloop for")
("respond", bpo::value<std::string>()->default_value("ready"), "how to respond on target (ready, yield, io, timer)")
("respond-timeout", bpo::value<unsigned>()->default_value(1), "the 'timer' respond timeout (us)")
("concurrency", bpo::value<unsigned>()->default_value(1), "smp::submit_to operations to issue in parallel")
;
return at.run(ac, av, [&at] {
auto duration = seconds(at.configuration()["duration"].as<unsigned>());
worker::config cfg;
cfg.targets = at.configuration()["targets"].as<unsigned>();
cfg.thinkers = at.configuration()["thinkers"].as<unsigned>();
cfg.think = microseconds(at.configuration()["think"].as<unsigned>());
cfg.respond = parse_respond_type(at.configuration()["respond"].as<std::string>());
cfg.respond_tmo = microseconds(at.configuration()["respond-timeout"].as<unsigned>());
cfg.concurrency = at.configuration()["concurrency"].as<unsigned>();
return async([cfg, duration] {
sharded<worker> workers;
auto start = steady_clock::now();
workers.start(cfg).get();
seastar::sleep(duration).get();
workers.invoke_on_all(&worker::stop).get();
auto real_duration = duration_cast<seconds>(steady_clock::now() - start);
fmt::print("took {}s (expected {}s)\n", real_duration.count(), duration.count());
stats st(real_duration.count()), st_targets(real_duration.count());
for (unsigned i = 0; i < smp::count; i++) {
workers.invoke_on(i, [&st, &st_targets] (worker& w) {
if (w.is_target()) {
st_targets.append(w.total());
} else {
st.append(w.total());
}
}).get();
}
fmt::print("workers({:2}): min {:.1f} avg {:.1f} max {:.1f} op/s\n", st.nr(), st.min(), st.avg(), st.max());
fmt::print("targets({:2}): min {:.1f} avg {:.1f} max {:.1f} op/s\n", st_targets.nr(), st_targets.min(), st_targets.avg(), st_targets.max());
workers.stop().get();
});
});
}