/* * 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 #include #include #include #include #include #include #include #include using namespace seastar; using namespace std::chrono; class thinker { class poisson_process { std::random_device _rd; std::mt19937 _rng; std::exponential_distribution _exp; public: poisson_process(duration period) : _rng(_rd()) , _exp(1.0 / period.count()) { } duration get() { return duration(_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 _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(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(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::max(); uint64_t _max = std::numeric_limits::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()->default_value(32), "time to run the test (seconds)") ("targets", bpo::value()->default_value(1), "number of responder shards") ("thinkers", bpo::value()->default_value(0), "thinker fibers to run in parallel on targets") ("think", bpo::value()->default_value(100), "time (us) thinkers busyloop for") ("respond", bpo::value()->default_value("ready"), "how to respond on target (ready, yield, io, timer)") ("respond-timeout", bpo::value()->default_value(1), "the 'timer' respond timeout (us)") ("concurrency", bpo::value()->default_value(1), "smp::submit_to operations to issue in parallel") ; return at.run(ac, av, [&at] { auto duration = seconds(at.configuration()["duration"].as()); worker::config cfg; cfg.targets = at.configuration()["targets"].as(); cfg.thinkers = at.configuration()["thinkers"].as(); cfg.think = microseconds(at.configuration()["think"].as()); cfg.respond = parse_respond_type(at.configuration()["respond"].as()); cfg.respond_tmo = microseconds(at.configuration()["respond-timeout"].as()); cfg.concurrency = at.configuration()["concurrency"].as(); return async([cfg, duration] { sharded 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(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(); }); }); }