From 41c07991803a44a5f17456e5b2c5ba1854908798 Mon Sep 17 00:00:00 2001 From: Pavel Emelyanov Date: Wed, 18 Sep 2024 18:25:18 +0300 Subject: [PATCH] test: Add perf test for http client The test opens a server socket that reads socket up until double CRLF and then responds back with "HTTP/1.1 200 OK host: test" line in a loop. Then runs http client against it measuring time it takes to perform the given amount of requests. It uses in-memory loopback sockets (seastar wrapper around seastar::queue). Signed-off-by: Pavel Emelyanov --- tests/perf/CMakeLists.txt | 4 + tests/perf/http_client_perf.cc | 151 +++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 tests/perf/http_client_perf.cc diff --git a/tests/perf/CMakeLists.txt b/tests/perf/CMakeLists.txt index 00c216e218f..853c9e4431d 100644 --- a/tests/perf/CMakeLists.txt +++ b/tests/perf/CMakeLists.txt @@ -88,3 +88,7 @@ seastar_add_test (allocator seastar_add_test (container SOURCES container_perf.cc) + +seastar_add_test (http_client + SOURCES http_client_perf.cc + NO_SEASTAR_PERF_TESTING_LIBRARY) diff --git a/tests/perf/http_client_perf.cc b/tests/perf/http_client_perf.cc new file mode 100644 index 00000000000..10622fcec8a --- /dev/null +++ b/tests/perf/http_client_perf.cc @@ -0,0 +1,151 @@ +/* + * 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) 2024 ScyllaDB + */ + +/* + * The test runs http::experimental::client against minimalistic (see below) server on + * one shard using single "in-memory" connection. + * + * The client sents one request at-a-time, waiting for the server response before sending + * the next one. + * + * The server is a fiber that runs on top of the raw connection, reads it up until double + * CRLF and then responds back with the "HTTP/1.1 200 OK host: test" line. So it's not + * http::server instance, but a lightweight mock. + * + * The connection is net::connected_socket wrapper over seastar::queue, not Linux socket. + */ + +#include +#include +#include +#include +#include +#include +#include +#include <../../tests/unit/loopback_socket.hh> +#include +#include + +using namespace seastar; +using namespace std::chrono_literals; + +class server { + seastar::server_socket _ss; + seastar::connected_socket _cs; + seastar::input_stream _in; + seastar::output_stream _out; + sstring _req; + + future<> run_serve_loop() { + while (true) { + temporary_buffer buf = co_await _in.read(); + if (buf.empty()) { + co_return; + } + + _req += sstring(buf.get(), buf.size()); + if (_req.ends_with("\r\n\r\n")) { + sstring r200("HTTP/1.1 200 OK\r\nHost: test\r\n\r\n"); + co_await _out.write(r200); + co_await _out.flush(); + _req = ""; + } + } + } + +public: + server(loopback_connection_factory& lcf) : _ss(lcf.get_server_socket()) {} + future<> serve() { + return _ss.accept().then([this] (seastar::accept_result ar) { + _cs = std::move(ar.connection); + _in = _cs.input(); + _out = _cs.output(); + return run_serve_loop().finally([this] { + return when_all(_in.close(), _out.close()).discard_result(); + }); + }); + } +}; + +class loopback_http_factory : public http::experimental::connection_factory { + loopback_socket_impl lsi; +public: + explicit loopback_http_factory(loopback_connection_factory& f) : lsi(f) {} + virtual future make(abort_source* as) override { + return lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())); + } +}; + +class client { + seastar::http::experimental::client _cln; + const unsigned _warmup_limit; + const unsigned _limit; + + future<> make_requests(unsigned nr) { + for (unsigned i = 0; i < nr; i++) { + auto req = http::request::make("GET", "test", "/test"); + co_await _cln.make_request(std::move(req), [] (const http::reply& rep, input_stream&& in) { + return make_ready_future<>(); + }, http::reply::status_type::ok); + } + } +public: + client(loopback_connection_factory& lcf, unsigned ops, unsigned warmup) + : _cln(std::make_unique(lcf)) + , _warmup_limit(warmup) + , _limit(ops) + {} + future<> work() { + fmt::print("Warming up with {} requests\n", _warmup_limit); + return make_requests(_warmup_limit).then([this] { + fmt::print("Warmup finished, making {} requests\n", _limit); + auto start = std::chrono::steady_clock::now(); + auto mallocs = memory::stats().mallocs(); + return make_requests(_limit).then([this, start, mallocs] { + auto delta = (std::chrono::steady_clock::now() - start) / _limit; + auto allocs = double(memory::stats().mallocs() - mallocs) / _limit; + fmt::print("Made {} requests, {:.3f} usec/op, {:.1f} allocs/op\n", _limit, + std::chrono::duration_cast>(delta).count(), allocs); + }); + }).finally([this] { + return _cln.close(); + }); + } +}; + +int main(int ac, char** av) { + app_template at; + namespace bpo = boost::program_options; + at.add_options() + ("total-ops", bpo::value()->default_value(1000000), "Total requests to make") + ("warmup-ops", bpo::value()->default_value(10000), "Requests to warm up") + ; + return at.run(ac, av, [&at] { + auto total_ops = at.configuration()["total-ops"].as(); + auto warmup_ops = at.configuration()["warmup-ops"].as(); + return seastar::async([total_ops, warmup_ops] { + loopback_connection_factory lcf(1); + server srv(lcf); + client cln(lcf, total_ops, warmup_ops); + when_all(srv.serve(), cln.work()).discard_result().get(); + }); + }); +}