diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 19:37:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-10 19:37:08 +0000 |
commit | d710a65c8b50bc3d4d0920dc6e865296f42edd5e (patch) | |
tree | d3bf9843448af9398b55f49a50a194bbaacd724e /src/h2load.cc | |
parent | Initial commit. (diff) | |
download | nghttp2-d710a65c8b50bc3d4d0920dc6e865296f42edd5e.tar.xz nghttp2-d710a65c8b50bc3d4d0920dc6e865296f42edd5e.zip |
Adding upstream version 1.59.0.upstream/1.59.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/h2load.cc')
-rw-r--r-- | src/h2load.cc | 3292 |
1 files changed, 3292 insertions, 0 deletions
diff --git a/src/h2load.cc b/src/h2load.cc new file mode 100644 index 0000000..0f07610 --- /dev/null +++ b/src/h2load.cc @@ -0,0 +1,3292 @@ +/* + * nghttp2 - HTTP/2 C Library + * + * Copyright (c) 2014 Tatsuhiro Tsujikawa + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +#include "h2load.h" + +#include <getopt.h> +#include <signal.h> +#ifdef HAVE_NETINET_IN_H +# include <netinet/in.h> +#endif // HAVE_NETINET_IN_H +#include <netinet/tcp.h> +#include <sys/stat.h> +#ifdef HAVE_FCNTL_H +# include <fcntl.h> +#endif // HAVE_FCNTL_H +#include <sys/mman.h> +#include <netinet/udp.h> + +#include <cstdio> +#include <cassert> +#include <cstdlib> +#include <iostream> +#include <iomanip> +#include <fstream> +#include <chrono> +#include <thread> +#include <future> +#include <random> + +#include <openssl/err.h> + +#ifdef ENABLE_HTTP3 +# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# include <ngtcp2/ngtcp2_crypto_quictls.h> +# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +# include <ngtcp2/ngtcp2_crypto_boringssl.h> +# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +#endif // ENABLE_HTTP3 + +#include "url-parser/url_parser.h" + +#include "h2load_http1_session.h" +#include "h2load_http2_session.h" +#ifdef ENABLE_HTTP3 +# include "h2load_http3_session.h" +# include "h2load_quic.h" +#endif // ENABLE_HTTP3 +#include "tls.h" +#include "http2.h" +#include "util.h" +#include "template.h" +#include "ssl_compat.h" + +#ifndef O_BINARY +# define O_BINARY (0) +#endif // O_BINARY + +using namespace nghttp2; + +namespace h2load { + +namespace { +bool recorded(const std::chrono::steady_clock::time_point &t) { + return std::chrono::steady_clock::duration::zero() != t.time_since_epoch(); +} +} // namespace + +namespace { +std::ofstream keylog_file; +void keylog_callback(const SSL *ssl, const char *line) { + keylog_file.write(line, strlen(line)); + keylog_file.put('\n'); + keylog_file.flush(); +} +} // namespace + +Config::Config() + : ciphers(tls::DEFAULT_CIPHER_LIST), + tls13_ciphers("TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_" + "CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256"), + groups("X25519:P-256:P-384:P-521"), + data_length(-1), + data(nullptr), + addrs(nullptr), + nreqs(1), + nclients(1), + nthreads(1), + max_concurrent_streams(1), + window_bits(30), + connection_window_bits(30), + max_frame_size(16_k), + rate(0), + rate_period(1.0), + duration(0.0), + warm_up_time(0.0), + conn_active_timeout(0.), + conn_inactivity_timeout(0.), + no_tls_proto(PROTO_HTTP2), + header_table_size(4_k), + encoder_header_table_size(4_k), + data_fd(-1), + log_fd(-1), + qlog_file_base(), + port(0), + default_port(0), + connect_to_port(0), + verbose(false), + timing_script(false), + base_uri_unix(false), + unix_addr{}, + rps(0.), + no_udp_gso(false), + max_udp_payload_size(0), + ktls(false) {} + +Config::~Config() { + if (addrs) { + if (base_uri_unix) { + delete addrs; + } else { + freeaddrinfo(addrs); + } + } + + if (data_fd != -1) { + close(data_fd); + } +} + +bool Config::is_rate_mode() const { return (this->rate != 0); } +bool Config::is_timing_based_mode() const { return (this->duration > 0); } +bool Config::has_base_uri() const { return (!this->base_uri.empty()); } +bool Config::rps_enabled() const { return this->rps > 0.0; } +bool Config::is_quic() const { +#ifdef ENABLE_HTTP3 + return !alpn_list.empty() && + (alpn_list[0] == NGHTTP3_ALPN_H3 || alpn_list[0] == "\x5h3-29"); +#else // !ENABLE_HTTP3 + return false; +#endif // !ENABLE_HTTP3 +} +Config config; + +namespace { +constexpr size_t MAX_SAMPLES = 1000000; +} // namespace + +Stats::Stats(size_t req_todo, size_t nclients) + : req_todo(req_todo), + req_started(0), + req_done(0), + req_success(0), + req_status_success(0), + req_failed(0), + req_error(0), + req_timedout(0), + bytes_total(0), + bytes_head(0), + bytes_head_decomp(0), + bytes_body(0), + status(), + udp_dgram_recv(0), + udp_dgram_sent(0) {} + +Stream::Stream() : req_stat{}, status_success(-1) {} + +namespace { +std::random_device rd; +} // namespace + +namespace { +std::mt19937 gen(rd()); +} // namespace + +namespace { +void sampling_init(Sampling &smp, size_t max_samples) { + smp.n = 0; + smp.max_samples = max_samples; +} +} // namespace + +namespace { +void writecb(struct ev_loop *loop, ev_io *w, int revents) { + auto client = static_cast<Client *>(w->data); + client->restart_timeout(); + auto rv = client->do_write(); + if (rv == Client::ERR_CONNECT_FAIL) { + client->disconnect(); + // Try next address + client->current_addr = nullptr; + rv = client->connect(); + if (rv != 0) { + client->fail(); + client->worker->free_client(client); + delete client; + return; + } + return; + } + if (rv != 0) { + client->fail(); + client->worker->free_client(client); + delete client; + } +} +} // namespace + +namespace { +void readcb(struct ev_loop *loop, ev_io *w, int revents) { + auto client = static_cast<Client *>(w->data); + client->restart_timeout(); + if (client->do_read() != 0) { + if (client->try_again_or_fail() == 0) { + return; + } + client->worker->free_client(client); + delete client; + return; + } + client->signal_write(); +} +} // namespace + +namespace { +// Called every rate_period when rate mode is being used +void rate_period_timeout_w_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + auto nclients_per_second = worker->rate; + auto conns_remaining = worker->nclients - worker->nconns_made; + auto nclients = std::min(nclients_per_second, conns_remaining); + + for (size_t i = 0; i < nclients; ++i) { + auto req_todo = worker->nreqs_per_client; + if (worker->nreqs_rem > 0) { + ++req_todo; + --worker->nreqs_rem; + } + auto client = + std::make_unique<Client>(worker->next_client_id++, worker, req_todo); + + ++worker->nconns_made; + + if (client->connect() != 0) { + std::cerr << "client could not connect to host" << std::endl; + client->fail(); + } else { + if (worker->config->is_timing_based_mode()) { + worker->clients.push_back(client.release()); + } else { + client.release(); + } + } + worker->report_rate_progress(); + } + if (!worker->config->is_timing_based_mode()) { + if (worker->nconns_made >= worker->nclients) { + ev_timer_stop(worker->loop, w); + } + } else { + // To check whether all created clients are pushed correctly + assert(worker->nclients == worker->clients.size()); + } +} +} // namespace + +namespace { +// Called when the duration for infinite number of requests are over +void duration_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + + worker->current_phase = Phase::DURATION_OVER; + + std::cout << "Main benchmark duration is over for thread #" << worker->id + << ". Stopping all clients." << std::endl; + worker->stop_all_clients(); + std::cout << "Stopped all clients for thread #" << worker->id << std::endl; +} +} // namespace + +namespace { +// Called when the warmup duration for infinite number of requests are over +void warmup_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto worker = static_cast<Worker *>(w->data); + + std::cout << "Warm-up phase is over for thread #" << worker->id << "." + << std::endl; + std::cout << "Main benchmark duration is started for thread #" << worker->id + << "." << std::endl; + assert(worker->stats.req_started == 0); + assert(worker->stats.req_done == 0); + + for (auto client : worker->clients) { + if (client) { + assert(client->req_todo == 0); + assert(client->req_left == 1); + assert(client->req_inflight == 0); + assert(client->req_started == 0); + assert(client->req_done == 0); + + client->record_client_start_time(); + client->clear_connect_times(); + client->record_connect_start_time(); + } + } + + worker->current_phase = Phase::MAIN_DURATION; + + ev_timer_start(worker->loop, &worker->duration_watcher); +} +} // namespace + +namespace { +void rps_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<Client *>(w->data); + auto &session = client->session; + + assert(!config.timing_script); + + if (client->req_left == 0) { + ev_timer_stop(loop, w); + return; + } + + auto now = std::chrono::steady_clock::now(); + auto d = now - client->rps_duration_started; + auto n = static_cast<size_t>( + round(std::chrono::duration<double>(d).count() * config.rps)); + client->rps_req_pending += n; + client->rps_duration_started += + util::duration_from(static_cast<double>(n) / config.rps); + + if (client->rps_req_pending == 0) { + return; + } + + auto nreq = session->max_concurrent_streams() - client->rps_req_inflight; + if (nreq == 0) { + return; + } + + nreq = config.is_timing_based_mode() ? std::max(nreq, client->req_left) + : std::min(nreq, client->req_left); + nreq = std::min(nreq, client->rps_req_pending); + + client->rps_req_inflight += nreq; + client->rps_req_pending -= nreq; + + for (; nreq > 0; --nreq) { + if (client->submit_request() != 0) { + client->process_request_failure(); + break; + } + } + + client->signal_write(); +} +} // namespace + +namespace { +// Called when an a connection has been inactive for a set period of time +// or a fixed amount of time after all requests have been made on a +// connection +void conn_timeout_cb(EV_P_ ev_timer *w, int revents) { + auto client = static_cast<Client *>(w->data); + + ev_timer_stop(client->worker->loop, &client->conn_inactivity_watcher); + ev_timer_stop(client->worker->loop, &client->conn_active_watcher); + + if (util::check_socket_connected(client->fd)) { + client->timeout(); + } +} +} // namespace + +namespace { +bool check_stop_client_request_timeout(Client *client, ev_timer *w) { + if (client->req_left == 0) { + // no more requests to make, stop timer + ev_timer_stop(client->worker->loop, w); + return true; + } + + return false; +} +} // namespace + +namespace { +void client_request_timeout_cb(struct ev_loop *loop, ev_timer *w, int revents) { + auto client = static_cast<Client *>(w->data); + + if (client->streams.size() >= (size_t)config.max_concurrent_streams) { + ev_timer_stop(client->worker->loop, w); + return; + } + + if (client->submit_request() != 0) { + ev_timer_stop(client->worker->loop, w); + client->process_request_failure(); + return; + } + client->signal_write(); + + if (check_stop_client_request_timeout(client, w)) { + return; + } + + auto duration = + config.timings[client->reqidx] - config.timings[client->reqidx - 1]; + + while (duration < std::chrono::duration<double>(1e-9)) { + if (client->submit_request() != 0) { + ev_timer_stop(client->worker->loop, w); + client->process_request_failure(); + return; + } + client->signal_write(); + if (check_stop_client_request_timeout(client, w)) { + return; + } + + duration = + config.timings[client->reqidx] - config.timings[client->reqidx - 1]; + } + + client->request_timeout_watcher.repeat = util::ev_tstamp_from(duration); + ev_timer_again(client->worker->loop, &client->request_timeout_watcher); +} +} // namespace + +Client::Client(uint32_t id, Worker *worker, size_t req_todo) + : wb(&worker->mcpool), + cstat{}, + worker(worker), + ssl(nullptr), +#ifdef ENABLE_HTTP3 + quic{}, +#endif // ENABLE_HTTP3 + next_addr(config.addrs), + current_addr(nullptr), + reqidx(0), + state(CLIENT_IDLE), + req_todo(req_todo), + req_left(req_todo), + req_inflight(0), + req_started(0), + req_done(0), + id(id), + fd(-1), + local_addr{}, + new_connection_requested(false), + final(false), + rps_req_pending(0), + rps_req_inflight(0) { + if (req_todo == 0) { // this means infinite number of requests are to be made + // This ensures that number of requests are unbounded + // Just a positive number is fine, we chose the first positive number + req_left = 1; + } + ev_io_init(&wev, writecb, 0, EV_WRITE); + ev_io_init(&rev, readcb, 0, EV_READ); + + wev.data = this; + rev.data = this; + + ev_timer_init(&conn_inactivity_watcher, conn_timeout_cb, 0., + worker->config->conn_inactivity_timeout); + conn_inactivity_watcher.data = this; + + ev_timer_init(&conn_active_watcher, conn_timeout_cb, + worker->config->conn_active_timeout, 0.); + conn_active_watcher.data = this; + + ev_timer_init(&request_timeout_watcher, client_request_timeout_cb, 0., 0.); + request_timeout_watcher.data = this; + + ev_timer_init(&rps_watcher, rps_cb, 0., 0.); + rps_watcher.data = this; + +#ifdef ENABLE_HTTP3 + ev_timer_init(&quic.pkt_timer, quic_pkt_timeout_cb, 0., 0.); + quic.pkt_timer.data = this; + + if (config.is_quic()) { + quic.tx.data = std::make_unique<uint8_t[]>(64_k); + } + + ngtcp2_ccerr_default(&quic.last_error); +#endif // ENABLE_HTTP3 +} + +Client::~Client() { + disconnect(); + +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + quic_free(); + } +#endif // ENABLE_HTTP3 + + if (ssl) { + SSL_free(ssl); + } + + worker->sample_client_stat(&cstat); + ++worker->client_smp.n; +} + +int Client::do_read() { return readfn(*this); } +int Client::do_write() { return writefn(*this); } + +int Client::make_socket(addrinfo *addr) { + int rv; + + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 + fd = util::create_nonblock_udp_socket(addr->ai_family); + if (fd == -1) { + return -1; + } + +# ifdef UDP_GRO + int val = 1; + if (setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val)) != 0) { + std::cerr << "setsockopt UDP_GRO failed" << std::endl; + return -1; + } +# endif // UDP_GRO + + rv = util::bind_any_addr_udp(fd, addr->ai_family); + if (rv != 0) { + close(fd); + fd = -1; + return -1; + } + + socklen_t addrlen = sizeof(local_addr.su.storage); + rv = getsockname(fd, &local_addr.su.sa, &addrlen); + if (rv == -1) { + return -1; + } + local_addr.len = addrlen; + + if (quic_init(&local_addr.su.sa, local_addr.len, addr->ai_addr, + addr->ai_addrlen) != 0) { + std::cerr << "quic_init failed" << std::endl; + return -1; + } +#endif // ENABLE_HTTP3 + } else { + fd = util::create_nonblock_socket(addr->ai_family); + if (fd == -1) { + return -1; + } + if (config.scheme == "https") { + if (!ssl) { + ssl = SSL_new(worker->ssl_ctx); + } + + SSL_set_connect_state(ssl); + } + } + + if (ssl && !util::numeric_host(config.host.c_str())) { + SSL_set_tlsext_host_name(ssl, config.host.c_str()); + } + + if (config.is_quic()) { + return 0; + } + + rv = ::connect(fd, addr->ai_addr, addr->ai_addrlen); + if (rv != 0 && errno != EINPROGRESS) { + if (ssl) { + SSL_free(ssl); + ssl = nullptr; + } + close(fd); + fd = -1; + return -1; + } + return 0; +} + +int Client::connect() { + int rv; + + if (!worker->config->is_timing_based_mode() || + worker->current_phase == Phase::MAIN_DURATION) { + record_client_start_time(); + clear_connect_times(); + record_connect_start_time(); + } else if (worker->current_phase == Phase::INITIAL_IDLE) { + worker->current_phase = Phase::WARM_UP; + std::cout << "Warm-up started for thread #" << worker->id << "." + << std::endl; + ev_timer_start(worker->loop, &worker->warmup_watcher); + } + + if (worker->config->conn_inactivity_timeout > 0.) { + ev_timer_again(worker->loop, &conn_inactivity_watcher); + } + + if (current_addr) { + rv = make_socket(current_addr); + if (rv == -1) { + return -1; + } + } else { + addrinfo *addr = nullptr; + while (next_addr) { + addr = next_addr; + next_addr = next_addr->ai_next; + rv = make_socket(addr); + if (rv == 0) { + break; + } + } + + if (fd == -1) { + return -1; + } + + assert(addr); + + current_addr = addr; + } + + ev_io_set(&rev, fd, EV_READ); + ev_io_set(&wev, fd, EV_WRITE); + + ev_io_start(worker->loop, &wev); + + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 + ev_io_start(worker->loop, &rev); + + readfn = &Client::read_quic; + writefn = &Client::write_quic; +#endif // ENABLE_HTTP3 + } else { + writefn = &Client::connected; + } + + return 0; +} + +void Client::timeout() { + process_timedout_streams(); + + disconnect(); +} + +void Client::restart_timeout() { + if (worker->config->conn_inactivity_timeout > 0.) { + ev_timer_again(worker->loop, &conn_inactivity_watcher); + } +} + +int Client::try_again_or_fail() { + disconnect(); + + if (new_connection_requested) { + new_connection_requested = false; + + if (req_left) { + + if (worker->current_phase == Phase::MAIN_DURATION) { + // At the moment, we don't have a facility to re-start request + // already in in-flight. Make them fail. + worker->stats.req_failed += req_inflight; + worker->stats.req_error += req_inflight; + + req_inflight = 0; + } + + // Keep using current address + if (connect() == 0) { + return 0; + } + std::cerr << "client could not connect to host" << std::endl; + } + } + + process_abandoned_streams(); + + return -1; +} + +void Client::fail() { + disconnect(); + + process_abandoned_streams(); +} + +void Client::disconnect() { + record_client_end_time(); + +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + quic_close_connection(); + } +#endif // ENABLE_HTTP3 + +#ifdef ENABLE_HTTP3 + ev_timer_stop(worker->loop, &quic.pkt_timer); +#endif // ENABLE_HTTP3 + ev_timer_stop(worker->loop, &conn_inactivity_watcher); + ev_timer_stop(worker->loop, &conn_active_watcher); + ev_timer_stop(worker->loop, &rps_watcher); + ev_timer_stop(worker->loop, &request_timeout_watcher); + streams.clear(); + session.reset(); + wb.reset(); + state = CLIENT_IDLE; + ev_io_stop(worker->loop, &wev); + ev_io_stop(worker->loop, &rev); + if (ssl) { + if (config.is_quic()) { + SSL_free(ssl); + ssl = nullptr; + } else { + SSL_set_shutdown(ssl, SSL_get_shutdown(ssl) | SSL_RECEIVED_SHUTDOWN); + ERR_clear_error(); + + if (SSL_shutdown(ssl) != 1) { + SSL_free(ssl); + ssl = nullptr; + } + } + } + if (fd != -1) { + shutdown(fd, SHUT_WR); + close(fd); + fd = -1; + } + + final = false; +} + +int Client::submit_request() { + if (session->submit_request() != 0) { + return -1; + } + + if (worker->current_phase != Phase::MAIN_DURATION) { + return 0; + } + + ++worker->stats.req_started; + ++req_started; + ++req_inflight; + if (!worker->config->is_timing_based_mode()) { + --req_left; + } + // if an active timeout is set and this is the last request to be submitted + // on this connection, start the active timeout. + if (worker->config->conn_active_timeout > 0. && req_left == 0) { + ev_timer_start(worker->loop, &conn_active_watcher); + } + + return 0; +} + +void Client::process_timedout_streams() { + if (worker->current_phase != Phase::MAIN_DURATION) { + return; + } + + for (auto &p : streams) { + auto &req_stat = p.second.req_stat; + if (!req_stat.completed) { + req_stat.stream_close_time = std::chrono::steady_clock::now(); + } + } + + worker->stats.req_timedout += req_inflight; + + process_abandoned_streams(); +} + +void Client::process_abandoned_streams() { + if (worker->current_phase != Phase::MAIN_DURATION) { + return; + } + + auto req_abandoned = req_inflight + req_left; + + worker->stats.req_failed += req_abandoned; + worker->stats.req_error += req_abandoned; + + req_inflight = 0; + req_left = 0; +} + +void Client::process_request_failure() { + if (worker->current_phase != Phase::MAIN_DURATION) { + return; + } + + worker->stats.req_failed += req_left; + worker->stats.req_error += req_left; + + req_left = 0; + + if (req_inflight == 0) { + terminate_session(); + } + std::cout << "Process Request Failure:" << worker->stats.req_failed + << std::endl; +} + +namespace { +void print_server_tmp_key(SSL *ssl) { + EVP_PKEY *key; + + if (!SSL_get_server_tmp_key(ssl, &key)) { + return; + } + + auto key_del = defer(EVP_PKEY_free, key); + + std::cout << "Server Temp Key: "; + + auto pkey_id = EVP_PKEY_id(key); + switch (pkey_id) { + case EVP_PKEY_RSA: + std::cout << "RSA " << EVP_PKEY_bits(key) << " bits" << std::endl; + break; + case EVP_PKEY_DH: + std::cout << "DH " << EVP_PKEY_bits(key) << " bits" << std::endl; + break; + case EVP_PKEY_EC: { +#if OPENSSL_3_0_0_API + std::array<char, 64> curve_name; + const char *cname; + if (!EVP_PKEY_get_utf8_string_param(key, "group", curve_name.data(), + curve_name.size(), nullptr)) { + cname = "<unknown>"; + } else { + cname = curve_name.data(); + } +#else // !OPENSSL_3_0_0_API + auto ec = EVP_PKEY_get1_EC_KEY(key); + auto ec_del = defer(EC_KEY_free, ec); + auto nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); + auto cname = EC_curve_nid2nist(nid); + if (!cname) { + cname = OBJ_nid2sn(nid); + } +#endif // !OPENSSL_3_0_0_API + + std::cout << "ECDH " << cname << " " << EVP_PKEY_bits(key) << " bits" + << std::endl; + break; + } + default: + std::cout << OBJ_nid2sn(pkey_id) << " " << EVP_PKEY_bits(key) << " bits" + << std::endl; + break; + } +} +} // namespace + +void Client::report_tls_info() { + if (worker->id == 0 && !worker->tls_info_report_done) { + worker->tls_info_report_done = true; + auto cipher = SSL_get_current_cipher(ssl); + std::cout << "TLS Protocol: " << tls::get_tls_protocol(ssl) << "\n" + << "Cipher: " << SSL_CIPHER_get_name(cipher) << std::endl; + print_server_tmp_key(ssl); + } +} + +void Client::report_app_info() { + if (worker->id == 0 && !worker->app_info_report_done) { + worker->app_info_report_done = true; + std::cout << "Application protocol: " << selected_proto << std::endl; + } +} + +void Client::terminate_session() { +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + quic.close_requested = true; + } +#endif // ENABLE_HTTP3 + if (session) { + session->terminate(); + } + // http1 session needs writecb to tear down session. + signal_write(); +} + +void Client::on_request(int32_t stream_id) { streams[stream_id] = Stream(); } + +void Client::on_header(int32_t stream_id, const uint8_t *name, size_t namelen, + const uint8_t *value, size_t valuelen) { + auto itr = streams.find(stream_id); + if (itr == std::end(streams)) { + return; + } + auto &stream = (*itr).second; + + if (worker->current_phase != Phase::MAIN_DURATION) { + // If the stream is for warm-up phase, then mark as a success + // But we do not update the count for 2xx, 3xx, etc status codes + // Same has been done in on_status_code function + stream.status_success = 1; + return; + } + + if (stream.status_success == -1 && namelen == 7 && + util::streq_l(":status", name, namelen)) { + int status = 0; + for (size_t i = 0; i < valuelen; ++i) { + if ('0' <= value[i] && value[i] <= '9') { + status *= 10; + status += value[i] - '0'; + if (status > 999) { + stream.status_success = 0; + return; + } + } else { + break; + } + } + + if (status < 200) { + return; + } + + stream.req_stat.status = status; + if (status >= 200 && status < 300) { + ++worker->stats.status[2]; + stream.status_success = 1; + } else if (status < 400) { + ++worker->stats.status[3]; + stream.status_success = 1; + } else if (status < 600) { + ++worker->stats.status[status / 100]; + stream.status_success = 0; + } else { + stream.status_success = 0; + } + } +} + +void Client::on_status_code(int32_t stream_id, uint16_t status) { + auto itr = streams.find(stream_id); + if (itr == std::end(streams)) { + return; + } + auto &stream = (*itr).second; + + if (worker->current_phase != Phase::MAIN_DURATION) { + stream.status_success = 1; + return; + } + + stream.req_stat.status = status; + if (status >= 200 && status < 300) { + ++worker->stats.status[2]; + stream.status_success = 1; + } else if (status < 400) { + ++worker->stats.status[3]; + stream.status_success = 1; + } else if (status < 600) { + ++worker->stats.status[status / 100]; + stream.status_success = 0; + } else { + stream.status_success = 0; + } +} + +void Client::on_stream_close(int32_t stream_id, bool success, bool final) { + if (worker->current_phase == Phase::MAIN_DURATION) { + if (req_inflight > 0) { + --req_inflight; + } + auto req_stat = get_req_stat(stream_id); + if (!req_stat) { + return; + } + + req_stat->stream_close_time = std::chrono::steady_clock::now(); + if (success) { + req_stat->completed = true; + ++worker->stats.req_success; + ++cstat.req_success; + + if (streams[stream_id].status_success == 1) { + ++worker->stats.req_status_success; + } else { + ++worker->stats.req_failed; + } + + worker->sample_req_stat(req_stat); + + // Count up in successful cases only + ++worker->request_times_smp.n; + } else { + ++worker->stats.req_failed; + ++worker->stats.req_error; + } + ++worker->stats.req_done; + ++req_done; + + if (worker->config->log_fd != -1) { + auto start = std::chrono::duration_cast<std::chrono::microseconds>( + req_stat->request_wall_time.time_since_epoch()); + auto delta = std::chrono::duration_cast<std::chrono::microseconds>( + req_stat->stream_close_time - req_stat->request_time); + + std::array<uint8_t, 256> buf; + auto p = std::begin(buf); + p = util::utos(p, start.count()); + *p++ = '\t'; + if (success) { + p = util::utos(p, req_stat->status); + } else { + *p++ = '-'; + *p++ = '1'; + } + *p++ = '\t'; + p = util::utos(p, delta.count()); + *p++ = '\n'; + + auto nwrite = static_cast<size_t>(std::distance(std::begin(buf), p)); + assert(nwrite <= buf.size()); + while (write(worker->config->log_fd, buf.data(), nwrite) == -1 && + errno == EINTR) + ; + } + } + + worker->report_progress(); + streams.erase(stream_id); + if (req_left == 0 && req_inflight == 0) { + terminate_session(); + return; + } + + if (!final && req_left > 0) { + if (config.timing_script) { + if (!ev_is_active(&request_timeout_watcher)) { + ev_feed_event(worker->loop, &request_timeout_watcher, EV_TIMER); + } + } else if (!config.rps_enabled()) { + if (submit_request() != 0) { + process_request_failure(); + } + } else if (rps_req_pending) { + --rps_req_pending; + if (submit_request() != 0) { + process_request_failure(); + } + } else { + assert(rps_req_inflight); + --rps_req_inflight; + } + } +} + +RequestStat *Client::get_req_stat(int32_t stream_id) { + auto it = streams.find(stream_id); + if (it == std::end(streams)) { + return nullptr; + } + + return &(*it).second.req_stat; +} + +int Client::connection_made() { + if (ssl) { + report_tls_info(); + + const unsigned char *next_proto = nullptr; + unsigned int next_proto_len; + + SSL_get0_alpn_selected(ssl, &next_proto, &next_proto_len); + + if (next_proto) { + auto proto = StringRef{next_proto, next_proto_len}; + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 + assert(session); + if (!util::streq(StringRef{&NGHTTP3_ALPN_H3[1]}, proto) && + !util::streq_l("h3-29", proto)) { + return -1; + } +#endif // ENABLE_HTTP3 + } else if (util::check_h2_is_selected(proto)) { + session = std::make_unique<Http2Session>(this); + } else if (util::streq(NGHTTP2_H1_1, proto)) { + session = std::make_unique<Http1Session>(this); + } + + // Just assign next_proto to selected_proto anyway to show the + // negotiation result. + selected_proto = proto.str(); + } else if (config.is_quic()) { + std::cerr << "QUIC requires ALPN negotiation" << std::endl; + return -1; + } else { + std::cout << "No protocol negotiated. Fallback behaviour may be activated" + << std::endl; + + for (const auto &proto : config.alpn_list) { + if (util::streq(NGHTTP2_H1_1_ALPN, StringRef{proto})) { + std::cout << "Server does not support ALPN. Falling back to HTTP/1.1." + << std::endl; + session = std::make_unique<Http1Session>(this); + selected_proto = NGHTTP2_H1_1.str(); + break; + } + } + } + + if (!selected_proto.empty()) { + report_app_info(); + } + + if (!session) { + std::cout + << "No supported protocol was negotiated. Supported protocols were:" + << std::endl; + for (const auto &proto : config.alpn_list) { + std::cout << proto.substr(1) << std::endl; + } + disconnect(); + return -1; + } + } else { + switch (config.no_tls_proto) { + case Config::PROTO_HTTP2: + session = std::make_unique<Http2Session>(this); + selected_proto = NGHTTP2_CLEARTEXT_PROTO_VERSION_ID; + break; + case Config::PROTO_HTTP1_1: + session = std::make_unique<Http1Session>(this); + selected_proto = NGHTTP2_H1_1.str(); + break; + default: + // unreachable + assert(0); + } + + report_app_info(); + } + + state = CLIENT_CONNECTED; + + session->on_connect(); + + record_connect_time(); + + if (config.rps_enabled()) { + rps_watcher.repeat = std::max(0.01, 1. / config.rps); + ev_timer_again(worker->loop, &rps_watcher); + rps_duration_started = std::chrono::steady_clock::now(); + } + + if (config.rps_enabled()) { + assert(req_left); + + ++rps_req_inflight; + + if (submit_request() != 0) { + process_request_failure(); + } + } else if (!config.timing_script) { + auto nreq = config.is_timing_based_mode() + ? std::max(req_left, session->max_concurrent_streams()) + : std::min(req_left, session->max_concurrent_streams()); + + for (; nreq > 0; --nreq) { + if (submit_request() != 0) { + process_request_failure(); + break; + } + } + } else { + + auto duration = config.timings[reqidx]; + + while (duration < std::chrono::duration<double>(1e-9)) { + if (submit_request() != 0) { + process_request_failure(); + break; + } + duration = config.timings[reqidx]; + if (reqidx == 0) { + // if reqidx wraps around back to 0, we uses up all lines and + // should break + break; + } + } + + if (duration >= std::chrono::duration<double>(1e-9)) { + // double check since we may have break due to reqidx wraps + // around back to 0 + request_timeout_watcher.repeat = util::ev_tstamp_from(duration); + ev_timer_again(worker->loop, &request_timeout_watcher); + } + } + signal_write(); + + return 0; +} + +int Client::on_read(const uint8_t *data, size_t len) { + auto rv = session->on_read(data, len); + if (rv != 0) { + return -1; + } + if (worker->current_phase == Phase::MAIN_DURATION) { + worker->stats.bytes_total += len; + } + signal_write(); + return 0; +} + +int Client::on_write() { + if (wb.rleft() >= BACKOFF_WRITE_BUFFER_THRES) { + return 0; + } + + if (session->on_write() != 0) { + return -1; + } + return 0; +} + +int Client::read_clear() { + uint8_t buf[8_k]; + + for (;;) { + ssize_t nread; + while ((nread = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR) + ; + if (nread == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 0; + } + return -1; + } + + if (nread == 0) { + return -1; + } + + if (on_read(buf, nread) != 0) { + return -1; + } + } + + return 0; +} + +int Client::write_clear() { + std::array<struct iovec, 2> iov; + + for (;;) { + if (on_write() != 0) { + return -1; + } + + auto iovcnt = wb.riovec(iov.data(), iov.size()); + + if (iovcnt == 0) { + break; + } + + ssize_t nwrite; + while ((nwrite = writev(fd, iov.data(), iovcnt)) == -1 && errno == EINTR) + ; + + if (nwrite == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + ev_io_start(worker->loop, &wev); + return 0; + } + return -1; + } + + wb.drain(nwrite); + } + + ev_io_stop(worker->loop, &wev); + + return 0; +} + +int Client::connected() { + if (!util::check_socket_connected(fd)) { + return ERR_CONNECT_FAIL; + } + ev_io_start(worker->loop, &rev); + ev_io_stop(worker->loop, &wev); + + if (ssl) { + SSL_set_fd(ssl, fd); + + readfn = &Client::tls_handshake; + writefn = &Client::tls_handshake; + + return do_write(); + } + + readfn = &Client::read_clear; + writefn = &Client::write_clear; + + if (connection_made() != 0) { + return -1; + } + + return 0; +} + +int Client::tls_handshake() { + ERR_clear_error(); + + auto rv = SSL_do_handshake(ssl); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + ev_io_stop(worker->loop, &wev); + return 0; + case SSL_ERROR_WANT_WRITE: + ev_io_start(worker->loop, &wev); + return 0; + default: + return -1; + } + } + + ev_io_stop(worker->loop, &wev); + + readfn = &Client::read_tls; + writefn = &Client::write_tls; + + if (connection_made() != 0) { + return -1; + } + + return 0; +} + +int Client::read_tls() { + uint8_t buf[8_k]; + + ERR_clear_error(); + + for (;;) { + auto rv = SSL_read(ssl, buf, sizeof(buf)); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + return 0; + case SSL_ERROR_WANT_WRITE: + // renegotiation started + return -1; + default: + return -1; + } + } + + if (on_read(buf, rv) != 0) { + return -1; + } + } +} + +int Client::write_tls() { + ERR_clear_error(); + + struct iovec iov; + + for (;;) { + if (on_write() != 0) { + return -1; + } + + auto iovcnt = wb.riovec(&iov, 1); + + if (iovcnt == 0) { + break; + } + + auto rv = SSL_write(ssl, iov.iov_base, iov.iov_len); + + if (rv <= 0) { + auto err = SSL_get_error(ssl, rv); + switch (err) { + case SSL_ERROR_WANT_READ: + // renegotiation started + return -1; + case SSL_ERROR_WANT_WRITE: + ev_io_start(worker->loop, &wev); + return 0; + default: + return -1; + } + } + + wb.drain(rv); + } + + ev_io_stop(worker->loop, &wev); + + return 0; +} + +#ifdef ENABLE_HTTP3 +// Returns 1 if sendmsg is blocked. +int Client::write_udp(const sockaddr *addr, socklen_t addrlen, + const uint8_t *data, size_t datalen, size_t gso_size) { + iovec msg_iov; + msg_iov.iov_base = const_cast<uint8_t *>(data); + msg_iov.iov_len = datalen; + + msghdr msg{}; + msg.msg_name = const_cast<sockaddr *>(addr); + msg.msg_namelen = addrlen; + msg.msg_iov = &msg_iov; + msg.msg_iovlen = 1; + +# ifdef UDP_SEGMENT + std::array<uint8_t, CMSG_SPACE(sizeof(uint16_t))> msg_ctrl{}; + if (gso_size && datalen > gso_size) { + msg.msg_control = msg_ctrl.data(); + msg.msg_controllen = msg_ctrl.size(); + + auto cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_UDP; + cm->cmsg_type = UDP_SEGMENT; + cm->cmsg_len = CMSG_LEN(sizeof(uint16_t)); + uint16_t n = gso_size; + memcpy(CMSG_DATA(cm), &n, sizeof(n)); + } +# endif // UDP_SEGMENT + + auto nwrite = sendmsg(fd, &msg, 0); + if (nwrite < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return 1; + } + + std::cerr << "sendmsg: errno=" << errno << std::endl; + } else { + ++worker->stats.udp_dgram_sent; + } + + ev_io_stop(worker->loop, &wev); + + return 0; +} +#endif // ENABLE_HTTP3 + +void Client::record_request_time(RequestStat *req_stat) { + req_stat->request_time = std::chrono::steady_clock::now(); + req_stat->request_wall_time = std::chrono::system_clock::now(); +} + +void Client::record_connect_start_time() { + cstat.connect_start_time = std::chrono::steady_clock::now(); +} + +void Client::record_connect_time() { + cstat.connect_time = std::chrono::steady_clock::now(); +} + +void Client::record_ttfb() { + if (recorded(cstat.ttfb)) { + return; + } + + cstat.ttfb = std::chrono::steady_clock::now(); +} + +void Client::clear_connect_times() { + cstat.connect_start_time = std::chrono::steady_clock::time_point(); + cstat.connect_time = std::chrono::steady_clock::time_point(); + cstat.ttfb = std::chrono::steady_clock::time_point(); +} + +void Client::record_client_start_time() { + // Record start time only once at the very first connection is going + // to be made. + if (recorded(cstat.client_start_time)) { + return; + } + + cstat.client_start_time = std::chrono::steady_clock::now(); +} + +void Client::record_client_end_time() { + // Unlike client_start_time, we overwrite client_end_time. This + // handles multiple connect/disconnect for HTTP/1.1 benchmark. + cstat.client_end_time = std::chrono::steady_clock::now(); +} + +void Client::signal_write() { ev_io_start(worker->loop, &wev); } + +void Client::try_new_connection() { new_connection_requested = true; } + +namespace { +int get_ev_loop_flags() { + if (ev_supported_backends() & ~ev_recommended_backends() & EVBACKEND_KQUEUE) { + return ev_recommended_backends() | EVBACKEND_KQUEUE; + } + + return 0; +} +} // namespace + +Worker::Worker(uint32_t id, SSL_CTX *ssl_ctx, size_t req_todo, size_t nclients, + size_t rate, size_t max_samples, Config *config) + : randgen(util::make_mt19937()), + stats(req_todo, nclients), + loop(ev_loop_new(get_ev_loop_flags())), + ssl_ctx(ssl_ctx), + config(config), + id(id), + tls_info_report_done(false), + app_info_report_done(false), + nconns_made(0), + nclients(nclients), + nreqs_per_client(req_todo / nclients), + nreqs_rem(req_todo % nclients), + rate(rate), + max_samples(max_samples), + next_client_id(0) { + if (!config->is_rate_mode() && !config->is_timing_based_mode()) { + progress_interval = std::max(static_cast<size_t>(1), req_todo / 10); + } else { + progress_interval = std::max(static_cast<size_t>(1), nclients / 10); + } + + // Below timeout is not needed in case of timing-based benchmarking + // create timer that will go off every rate_period + ev_timer_init(&timeout_watcher, rate_period_timeout_w_cb, 0., + config->rate_period); + timeout_watcher.data = this; + + if (config->is_timing_based_mode()) { + stats.req_stats.reserve(std::max(req_todo, max_samples)); + stats.client_stats.reserve(std::max(nclients, max_samples)); + } else { + stats.req_stats.reserve(std::min(req_todo, max_samples)); + stats.client_stats.reserve(std::min(nclients, max_samples)); + } + + sampling_init(request_times_smp, max_samples); + sampling_init(client_smp, max_samples); + + ev_timer_init(&duration_watcher, duration_timeout_cb, config->duration, 0.); + duration_watcher.data = this; + + ev_timer_init(&warmup_watcher, warmup_timeout_cb, config->warm_up_time, 0.); + warmup_watcher.data = this; + + if (config->is_timing_based_mode()) { + current_phase = Phase::INITIAL_IDLE; + } else { + current_phase = Phase::MAIN_DURATION; + } +} + +Worker::~Worker() { + ev_timer_stop(loop, &timeout_watcher); + ev_timer_stop(loop, &duration_watcher); + ev_timer_stop(loop, &warmup_watcher); + ev_loop_destroy(loop); +} + +void Worker::stop_all_clients() { + for (auto client : clients) { + if (client) { + client->terminate_session(); + } + } +} + +void Worker::free_client(Client *deleted_client) { + for (auto &client : clients) { + if (client == deleted_client) { + client->req_todo = client->req_done; + stats.req_todo += client->req_todo; + auto index = &client - &clients[0]; + clients[index] = nullptr; + return; + } + } +} + +void Worker::run() { + if (!config->is_rate_mode() && !config->is_timing_based_mode()) { + for (size_t i = 0; i < nclients; ++i) { + auto req_todo = nreqs_per_client; + if (nreqs_rem > 0) { + ++req_todo; + --nreqs_rem; + } + + auto client = std::make_unique<Client>(next_client_id++, this, req_todo); + if (client->connect() != 0) { + std::cerr << "client could not connect to host" << std::endl; + client->fail(); + } else { + client.release(); + } + } + } else if (config->is_rate_mode()) { + ev_timer_again(loop, &timeout_watcher); + + // call callback so that we don't waste the first rate_period + rate_period_timeout_w_cb(loop, &timeout_watcher, 0); + } else { + // call the callback to start for one single time + rate_period_timeout_w_cb(loop, &timeout_watcher, 0); + } + ev_run(loop, 0); +} + +namespace { +template <typename Stats, typename Stat> +void sample(Sampling &smp, Stats &stats, Stat *s) { + ++smp.n; + if (stats.size() < smp.max_samples) { + stats.push_back(*s); + return; + } + auto d = std::uniform_int_distribution<unsigned long>(0, smp.n - 1); + auto i = d(gen); + if (i < smp.max_samples) { + stats[i] = *s; + } +} +} // namespace + +void Worker::sample_req_stat(RequestStat *req_stat) { + sample(request_times_smp, stats.req_stats, req_stat); +} + +void Worker::sample_client_stat(ClientStat *cstat) { + sample(client_smp, stats.client_stats, cstat); +} + +void Worker::report_progress() { + if (id != 0 || config->is_rate_mode() || stats.req_done % progress_interval || + config->is_timing_based_mode()) { + return; + } + + std::cout << "progress: " << stats.req_done * 100 / stats.req_todo << "% done" + << std::endl; +} + +void Worker::report_rate_progress() { + if (id != 0 || nconns_made % progress_interval) { + return; + } + + std::cout << "progress: " << nconns_made * 100 / nclients + << "% of clients started" << std::endl; +} + +namespace { +// Returns percentage of number of samples within mean +/- sd. +double within_sd(const std::vector<double> &samples, double mean, double sd) { + if (samples.size() == 0) { + return 0.0; + } + auto lower = mean - sd; + auto upper = mean + sd; + auto m = std::count_if( + std::begin(samples), std::end(samples), + [&lower, &upper](double t) { return lower <= t && t <= upper; }); + return (m / static_cast<double>(samples.size())) * 100; +} +} // namespace + +namespace { +// Computes statistics using |samples|. The min, max, mean, sd, and +// percentage of number of samples within mean +/- sd are computed. +// If |sampling| is true, this computes sample variance. Otherwise, +// population variance. +SDStat compute_time_stat(const std::vector<double> &samples, + bool sampling = false) { + if (samples.empty()) { + return {0.0, 0.0, 0.0, 0.0, 0.0}; + } + // standard deviation calculated using Rapid calculation method: + // https://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods + double a = 0, q = 0; + size_t n = 0; + double sum = 0; + auto res = SDStat{std::numeric_limits<double>::max(), + std::numeric_limits<double>::min()}; + for (const auto &t : samples) { + ++n; + res.min = std::min(res.min, t); + res.max = std::max(res.max, t); + sum += t; + + auto na = a + (t - a) / n; + q += (t - a) * (t - na); + a = na; + } + + assert(n > 0); + res.mean = sum / n; + res.sd = sqrt(q / (sampling && n > 1 ? n - 1 : n)); + res.within_sd = within_sd(samples, res.mean, res.sd); + + return res; +} +} // namespace + +namespace { +SDStats +process_time_stats(const std::vector<std::unique_ptr<Worker>> &workers) { + auto request_times_sampling = false; + auto client_times_sampling = false; + size_t nrequest_times = 0; + size_t nclient_times = 0; + for (const auto &w : workers) { + nrequest_times += w->stats.req_stats.size(); + request_times_sampling = w->request_times_smp.n > w->stats.req_stats.size(); + + nclient_times += w->stats.client_stats.size(); + client_times_sampling = w->client_smp.n > w->stats.client_stats.size(); + } + + std::vector<double> request_times; + request_times.reserve(nrequest_times); + + std::vector<double> connect_times, ttfb_times, rps_values; + connect_times.reserve(nclient_times); + ttfb_times.reserve(nclient_times); + rps_values.reserve(nclient_times); + + for (const auto &w : workers) { + for (const auto &req_stat : w->stats.req_stats) { + if (!req_stat.completed) { + continue; + } + request_times.push_back( + std::chrono::duration_cast<std::chrono::duration<double>>( + req_stat.stream_close_time - req_stat.request_time) + .count()); + } + + const auto &stat = w->stats; + + for (const auto &cstat : stat.client_stats) { + if (recorded(cstat.client_start_time) && + recorded(cstat.client_end_time)) { + auto t = std::chrono::duration_cast<std::chrono::duration<double>>( + cstat.client_end_time - cstat.client_start_time) + .count(); + if (t > 1e-9) { + rps_values.push_back(cstat.req_success / t); + } + } + + // We will get connect event before FFTB. + if (!recorded(cstat.connect_start_time) || + !recorded(cstat.connect_time)) { + continue; + } + + connect_times.push_back( + std::chrono::duration_cast<std::chrono::duration<double>>( + cstat.connect_time - cstat.connect_start_time) + .count()); + + if (!recorded(cstat.ttfb)) { + continue; + } + + ttfb_times.push_back( + std::chrono::duration_cast<std::chrono::duration<double>>( + cstat.ttfb - cstat.connect_start_time) + .count()); + } + } + + return {compute_time_stat(request_times, request_times_sampling), + compute_time_stat(connect_times, client_times_sampling), + compute_time_stat(ttfb_times, client_times_sampling), + compute_time_stat(rps_values, client_times_sampling)}; +} +} // namespace + +namespace { +void resolve_host() { + if (config.base_uri_unix) { + auto res = std::make_unique<addrinfo>(); + res->ai_family = config.unix_addr.sun_family; + res->ai_socktype = SOCK_STREAM; + res->ai_addrlen = sizeof(config.unix_addr); + res->ai_addr = + static_cast<struct sockaddr *>(static_cast<void *>(&config.unix_addr)); + + config.addrs = res.release(); + return; + }; + + int rv; + addrinfo hints{}, *res; + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = 0; + hints.ai_flags = AI_ADDRCONFIG; + + const auto &resolve_host = + config.connect_to_host.empty() ? config.host : config.connect_to_host; + auto port = + config.connect_to_port == 0 ? config.port : config.connect_to_port; + + rv = + getaddrinfo(resolve_host.c_str(), util::utos(port).c_str(), &hints, &res); + if (rv != 0) { + std::cerr << "getaddrinfo() failed: " << gai_strerror(rv) << std::endl; + exit(EXIT_FAILURE); + } + if (res == nullptr) { + std::cerr << "No address returned" << std::endl; + exit(EXIT_FAILURE); + } + config.addrs = res; +} +} // namespace + +namespace { +std::string get_reqline(const char *uri, const http_parser_url &u) { + std::string reqline; + + if (util::has_uri_field(u, UF_PATH)) { + reqline = util::get_uri_field(uri, u, UF_PATH).str(); + } else { + reqline = "/"; + } + + if (util::has_uri_field(u, UF_QUERY)) { + reqline += '?'; + reqline += util::get_uri_field(uri, u, UF_QUERY); + } + + return reqline; +} +} // namespace + +namespace { +constexpr char UNIX_PATH_PREFIX[] = "unix:"; +} // namespace + +namespace { +bool parse_base_uri(const StringRef &base_uri) { + http_parser_url u{}; + if (http_parser_parse_url(base_uri.c_str(), base_uri.size(), 0, &u) != 0 || + !util::has_uri_field(u, UF_SCHEMA) || !util::has_uri_field(u, UF_HOST)) { + return false; + } + + config.scheme = util::get_uri_field(base_uri.c_str(), u, UF_SCHEMA).str(); + config.host = util::get_uri_field(base_uri.c_str(), u, UF_HOST).str(); + config.default_port = util::get_default_port(base_uri.c_str(), u); + if (util::has_uri_field(u, UF_PORT)) { + config.port = u.port; + } else { + config.port = config.default_port; + } + + return true; +} +} // namespace +namespace { +// Use std::vector<std::string>::iterator explicitly, without that, +// http_parser_url u{} fails with clang-3.4. +std::vector<std::string> parse_uris(std::vector<std::string>::iterator first, + std::vector<std::string>::iterator last) { + std::vector<std::string> reqlines; + + if (first == last) { + std::cerr << "no URI available" << std::endl; + exit(EXIT_FAILURE); + } + + if (!config.has_base_uri()) { + + if (!parse_base_uri(StringRef{*first})) { + std::cerr << "invalid URI: " << *first << std::endl; + exit(EXIT_FAILURE); + } + + config.base_uri = *first; + } + + for (; first != last; ++first) { + http_parser_url u{}; + + auto uri = (*first).c_str(); + + if (http_parser_parse_url(uri, (*first).size(), 0, &u) != 0) { + std::cerr << "invalid URI: " << uri << std::endl; + exit(EXIT_FAILURE); + } + + reqlines.push_back(get_reqline(uri, u)); + } + + return reqlines; +} +} // namespace + +namespace { +std::vector<std::string> read_uri_from_file(std::istream &infile) { + std::vector<std::string> uris; + std::string line_uri; + while (std::getline(infile, line_uri)) { + uris.push_back(line_uri); + } + + return uris; +} +} // namespace + +namespace { +void read_script_from_file( + std::istream &infile, + std::vector<std::chrono::steady_clock::duration> &timings, + std::vector<std::string> &uris) { + std::string script_line; + int line_count = 0; + while (std::getline(infile, script_line)) { + line_count++; + if (script_line.empty()) { + std::cerr << "Empty line detected at line " << line_count + << ". Ignoring and continuing." << std::endl; + continue; + } + + std::size_t pos = script_line.find("\t"); + if (pos == std::string::npos) { + std::cerr << "Invalid line format detected, no tab character at line " + << line_count << ". \n\t" << script_line << std::endl; + exit(EXIT_FAILURE); + } + + const char *start = script_line.c_str(); + char *end; + auto v = std::strtod(start, &end); + + errno = 0; + if (v < 0.0 || !std::isfinite(v) || end == start || errno != 0) { + auto error = errno; + std::cerr << "Time value error at line " << line_count << ". \n\t" + << "value = " << script_line.substr(0, pos) << std::endl; + if (error != 0) { + std::cerr << "\t" << strerror(error) << std::endl; + } + exit(EXIT_FAILURE); + } + + timings.emplace_back( + std::chrono::duration_cast<std::chrono::steady_clock::duration>( + std::chrono::duration<double, std::milli>(v))); + uris.push_back(script_line.substr(pos + 1, script_line.size())); + } +} +} // namespace + +namespace { +std::unique_ptr<Worker> create_worker(uint32_t id, SSL_CTX *ssl_ctx, + size_t nreqs, size_t nclients, + size_t rate, size_t max_samples) { + std::stringstream rate_report; + if (config.is_rate_mode() && nclients > rate) { + rate_report << "Up to " << rate << " client(s) will be created every " + << util::duration_str(config.rate_period) << " "; + } + + if (config.is_timing_based_mode()) { + std::cout << "spawning thread #" << id << ": " << nclients + << " total client(s). Timing-based test with " + << config.warm_up_time << "s of warm-up time and " + << config.duration << "s of main duration for measurements." + << std::endl; + } else { + std::cout << "spawning thread #" << id << ": " << nclients + << " total client(s). " << rate_report.str() << nreqs + << " total requests" << std::endl; + } + + if (config.is_rate_mode()) { + return std::make_unique<Worker>(id, ssl_ctx, nreqs, nclients, rate, + max_samples, &config); + } else { + // Here rate is same as client because the rate_timeout callback + // will be called only once + return std::make_unique<Worker>(id, ssl_ctx, nreqs, nclients, nclients, + max_samples, &config); + } +} +} // namespace + +namespace { +int parse_header_table_size(uint32_t &dst, const char *opt, + const char *optarg) { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--" << opt << ": Bad option value: " << optarg << std::endl; + return -1; + } + if (n > std::numeric_limits<uint32_t>::max()) { + std::cerr << "--" << opt + << ": Value too large. It should be less than or equal to " + << std::numeric_limits<uint32_t>::max() << std::endl; + return -1; + } + + dst = n; + + return 0; +} +} // namespace + +namespace { +std::string make_http_authority(const Config &config) { + std::string host; + + if (util::numeric_host(config.host.c_str(), AF_INET6)) { + host += '['; + host += config.host; + host += ']'; + } else { + host = config.host; + } + + if (config.port != config.default_port) { + host += ':'; + host += util::utos(config.port); + } + + return host; +} +} // namespace + +namespace { +void print_version(std::ostream &out) { + out << "h2load nghttp2/" NGHTTP2_VERSION << std::endl; +} +} // namespace + +namespace { +void print_usage(std::ostream &out) { + out << R"(Usage: h2load [OPTIONS]... [URI]... +benchmarking tool for HTTP/2 server)" + << std::endl; +} +} // namespace + +namespace { +constexpr char DEFAULT_ALPN_LIST[] = "h2,h2-16,h2-14,http/1.1"; +} // namespace + +namespace { +void print_help(std::ostream &out) { + print_usage(out); + + auto config = Config(); + + out << R"( + <URI> Specify URI to access. Multiple URIs can be specified. + URIs are used in this order for each client. All URIs + are used, then first URI is used and then 2nd URI, and + so on. The scheme, host and port in the subsequent + URIs, if present, are ignored. Those in the first URI + are used solely. Definition of a base URI overrides all + scheme, host or port values. +Options: + -n, --requests=<N> + Number of requests across all clients. If it is used + with --timing-script-file option, this option specifies + the number of requests each client performs rather than + the number of requests across all clients. This option + is ignored if timing-based benchmarking is enabled (see + --duration option). + Default: )" + << config.nreqs << R"( + -c, --clients=<N> + Number of concurrent clients. With -r option, this + specifies the maximum number of connections to be made. + Default: )" + << config.nclients << R"( + -t, --threads=<N> + Number of native threads. + Default: )" + << config.nthreads << R"( + -i, --input-file=<PATH> + Path of a file with multiple URIs are separated by EOLs. + This option will disable URIs getting from command-line. + If '-' is given as <PATH>, URIs will be read from stdin. + URIs are used in this order for each client. All URIs + are used, then first URI is used and then 2nd URI, and + so on. The scheme, host and port in the subsequent + URIs, if present, are ignored. Those in the first URI + are used solely. Definition of a base URI overrides all + scheme, host or port values. + -m, --max-concurrent-streams=<N> + Max concurrent streams to issue per session. When + http/1.1 is used, this specifies the number of HTTP + pipelining requests in-flight. + Default: 1 + -f, --max-frame-size=<SIZE> + Maximum frame size that the local endpoint is willing to + receive. + Default: )" + << util::utos_unit(config.max_frame_size) << R"( + -w, --window-bits=<N> + Sets the stream level initial window size to (2**<N>)-1. + For QUIC, <N> is capped to 26 (roughly 64MiB). + Default: )" + << config.window_bits << R"( + -W, --connection-window-bits=<N> + Sets the connection level initial window size to + (2**<N>)-1. + Default: )" + << config.connection_window_bits << R"( + -H, --header=<HEADER> + Add/Override a header to the requests. + --ciphers=<SUITE> + Set allowed cipher list for TLSv1.2 or earlier. The + format of the string is described in OpenSSL ciphers(1). + Default: )" + << config.ciphers << R"( + --tls13-ciphers=<SUITE> + Set allowed cipher list for TLSv1.3. The format of the + string is described in OpenSSL ciphers(1). + Default: )" + << config.tls13_ciphers << R"( + -p, --no-tls-proto=<PROTOID> + Specify ALPN identifier of the protocol to be used when + accessing http URI without SSL/TLS. + Available protocols: )" + << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( and )" << NGHTTP2_H1_1 << R"( + Default: )" + << NGHTTP2_CLEARTEXT_PROTO_VERSION_ID << R"( + -d, --data=<PATH> + Post FILE to server. The request method is changed to + POST. For http/1.1 connection, if -d is used, the + maximum number of in-flight pipelined requests is set to + 1. + -r, --rate=<N> + Specifies the fixed rate at which connections are + created. The rate must be a positive integer, + representing the number of connections to be made per + rate period. The maximum number of connections to be + made is given in -c option. This rate will be + distributed among threads as evenly as possible. For + example, with -t2 and -r4, each thread gets 2 + connections per period. When the rate is 0, the program + will run as it normally does, creating connections at + whatever variable rate it wants. The default value for + this option is 0. -r and -D are mutually exclusive. + --rate-period=<DURATION> + Specifies the time period between creating connections. + The period must be a positive number, representing the + length of the period in time. This option is ignored if + the rate option is not used. The default value for this + option is 1s. + -D, --duration=<DURATION> + Specifies the main duration for the measurements in case + of timing-based benchmarking. -D and -r are mutually + exclusive. + --warm-up-time=<DURATION> + Specifies the time period before starting the actual + measurements, in case of timing-based benchmarking. + Needs to provided along with -D option. + -T, --connection-active-timeout=<DURATION> + Specifies the maximum time that h2load is willing to + keep a connection open, regardless of the activity on + said connection. <DURATION> must be a positive integer, + specifying the amount of time to wait. When no timeout + value is set (either active or inactive), h2load will + keep a connection open indefinitely, waiting for a + response. + -N, --connection-inactivity-timeout=<DURATION> + Specifies the amount of time that h2load is willing to + wait to see activity on a given connection. <DURATION> + must be a positive integer, specifying the amount of + time to wait. When no timeout value is set (either + active or inactive), h2load will keep a connection open + indefinitely, waiting for a response. + --timing-script-file=<PATH> + Path of a file containing one or more lines separated by + EOLs. Each script line is composed of two tab-separated + fields. The first field represents the time offset from + the start of execution, expressed as a positive value of + milliseconds with microsecond resolution. The second + field represents the URI. This option will disable URIs + getting from command-line. If '-' is given as <PATH>, + script lines will be read from stdin. Script lines are + used in order for each client. If -n is given, it must + be less than or equal to the number of script lines, + larger values are clamped to the number of script lines. + If -n is not given, the number of requests will default + to the number of script lines. The scheme, host and + port defined in the first URI are used solely. Values + contained in other URIs, if present, are ignored. + Definition of a base URI overrides all scheme, host or + port values. --timing-script-file and --rps are + mutually exclusive. + -B, --base-uri=(<URI>|unix:<PATH>) + Specify URI from which the scheme, host and port will be + used for all requests. The base URI overrides all + values defined either at the command line or inside + input files. If argument starts with "unix:", then the + rest of the argument will be treated as UNIX domain + socket path. The connection is made through that path + instead of TCP. In this case, scheme is inferred from + the first URI appeared in the command line or inside + input files as usual. + --alpn-list=<LIST> + Comma delimited list of ALPN protocol identifier sorted + in the order of preference. That means most desirable + protocol comes first. The parameter must be delimited + by a single comma only and any white spaces are treated + as a part of protocol string. + Default: )" + << DEFAULT_ALPN_LIST << R"( + --h1 Short hand for --alpn-list=http/1.1 + --no-tls-proto=http/1.1, which effectively force + http/1.1 for both http and https URI. + --header-table-size=<SIZE> + Specify decoder header table size. + Default: )" + << util::utos_unit(config.header_table_size) << R"( + --encoder-header-table-size=<SIZE> + Specify encoder header table size. The decoder (server) + specifies the maximum dynamic table size it accepts. + Then the negotiated dynamic table size is the minimum of + this option value and the value which server specified. + Default: )" + << util::utos_unit(config.encoder_header_table_size) << R"( + --log-file=<PATH> + Write per-request information to a file as tab-separated + columns: start time as microseconds since epoch; HTTP + status code; microseconds until end of response. More + columns may be added later. Rows are ordered by end-of- + response time when using one worker thread, but may + appear slightly out of order with multiple threads due + to buffering. Status code is -1 for failed streams. + --qlog-file-base=<PATH> + Enable qlog output and specify base file name for qlogs. + Qlog is emitted for each connection. For a given base + name "base", each output file name becomes + "base.M.N.sqlog" where M is worker ID and N is client ID + (e.g. "base.0.3.sqlog"). Only effective in QUIC runs. + --connect-to=<HOST>[:<PORT>] + Host and port to connect instead of using the authority + in <URI>. + --rps=<N> Specify request per second for each client. --rps and + --timing-script-file are mutually exclusive. + --groups=<GROUPS> + Specify the supported groups. + Default: )" + << config.groups << R"( + --no-udp-gso + Disable UDP GSO. + --max-udp-payload-size=<SIZE> + Specify the maximum outgoing UDP datagram payload size. + --ktls Enable ktls. + -v, --verbose + Output debug information. + --version Display version information and exit. + -h, --help Display this help and exit. + +-- + + The <SIZE> argument is an integer and an optional unit (e.g., 10K is + 10 * 1024). Units are K, M and G (powers of 1024). + + The <DURATION> argument is an integer and an optional unit (e.g., 1s + is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms + (hours, minutes, seconds and milliseconds, respectively). If a unit + is omitted, a second is used as unit.)" + << std::endl; +} +} // namespace + +int main(int argc, char **argv) { + std::string datafile; + std::string logfile; + bool nreqs_set_manually = false; + while (1) { + static int flag = 0; + constexpr static option long_options[] = { + {"requests", required_argument, nullptr, 'n'}, + {"clients", required_argument, nullptr, 'c'}, + {"data", required_argument, nullptr, 'd'}, + {"threads", required_argument, nullptr, 't'}, + {"max-concurrent-streams", required_argument, nullptr, 'm'}, + {"window-bits", required_argument, nullptr, 'w'}, + {"max-frame-size", required_argument, nullptr, 'f'}, + {"connection-window-bits", required_argument, nullptr, 'W'}, + {"input-file", required_argument, nullptr, 'i'}, + {"header", required_argument, nullptr, 'H'}, + {"no-tls-proto", required_argument, nullptr, 'p'}, + {"verbose", no_argument, nullptr, 'v'}, + {"help", no_argument, nullptr, 'h'}, + {"version", no_argument, &flag, 1}, + {"ciphers", required_argument, &flag, 2}, + {"rate", required_argument, nullptr, 'r'}, + {"connection-active-timeout", required_argument, nullptr, 'T'}, + {"connection-inactivity-timeout", required_argument, nullptr, 'N'}, + {"duration", required_argument, nullptr, 'D'}, + {"timing-script-file", required_argument, &flag, 3}, + {"base-uri", required_argument, nullptr, 'B'}, + {"npn-list", required_argument, &flag, 4}, + {"rate-period", required_argument, &flag, 5}, + {"h1", no_argument, &flag, 6}, + {"header-table-size", required_argument, &flag, 7}, + {"encoder-header-table-size", required_argument, &flag, 8}, + {"warm-up-time", required_argument, &flag, 9}, + {"log-file", required_argument, &flag, 10}, + {"connect-to", required_argument, &flag, 11}, + {"rps", required_argument, &flag, 12}, + {"groups", required_argument, &flag, 13}, + {"tls13-ciphers", required_argument, &flag, 14}, + {"no-udp-gso", no_argument, &flag, 15}, + {"qlog-file-base", required_argument, &flag, 16}, + {"max-udp-payload-size", required_argument, &flag, 17}, + {"ktls", no_argument, &flag, 18}, + {"alpn-list", required_argument, &flag, 19}, + {nullptr, 0, nullptr, 0}}; + int option_index = 0; + auto c = getopt_long(argc, argv, + "hvW:c:d:m:n:p:t:w:f:H:i:r:T:N:D:B:", long_options, + &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'n': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-n: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.nreqs = n; + nreqs_set_manually = true; + break; + } + case 'c': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-c: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.nclients = n; + break; + } + case 'd': + datafile = optarg; + break; + case 't': { +#ifdef NOTHREADS + std::cerr << "-t: WARNING: Threading disabled at build time, " + << "no threads created." << std::endl; +#else + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-t: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.nthreads = n; +#endif // NOTHREADS + break; + } + case 'm': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-m: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.max_concurrent_streams = n; + break; + } + case 'w': + case 'W': { + auto n = util::parse_uint(optarg); + if (n == -1 || n > 30) { + std::cerr << "-" << static_cast<char>(c) + << ": specify the integer in the range [0, 30], inclusive" + << std::endl; + exit(EXIT_FAILURE); + } + if (c == 'w') { + config.window_bits = n; + } else { + config.connection_window_bits = n; + } + break; + } + case 'f': { + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--max-frame-size: bad option value: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + if (static_cast<uint64_t>(n) < 16_k) { + std::cerr << "--max-frame-size: minimum 16384" << std::endl; + exit(EXIT_FAILURE); + } + if (static_cast<uint64_t>(n) > 16_m - 1) { + std::cerr << "--max-frame-size: maximum 16777215" << std::endl; + exit(EXIT_FAILURE); + } + config.max_frame_size = n; + break; + } + case 'H': { + char *header = optarg; + // Skip first possible ':' in the header name + char *value = strchr(optarg + 1, ':'); + if (!value || (header[0] == ':' && header + 1 == value)) { + std::cerr << "-H: invalid header: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + *value = 0; + value++; + while (isspace(*value)) { + value++; + } + if (*value == 0) { + // This could also be a valid case for suppressing a header + // similar to curl + std::cerr << "-H: invalid header - value missing: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + // Note that there is no processing currently to handle multiple + // message-header fields with the same field name + config.custom_headers.emplace_back(header, value); + util::inp_strlower(config.custom_headers.back().name); + break; + } + case 'i': + config.ifile = optarg; + break; + case 'p': { + auto proto = StringRef{optarg}; + if (util::strieq(StringRef::from_lit(NGHTTP2_CLEARTEXT_PROTO_VERSION_ID), + proto)) { + config.no_tls_proto = Config::PROTO_HTTP2; + } else if (util::strieq(NGHTTP2_H1_1, proto)) { + config.no_tls_proto = Config::PROTO_HTTP1_1; + } else { + std::cerr << "-p: unsupported protocol " << proto << std::endl; + exit(EXIT_FAILURE); + } + break; + } + case 'r': { + auto n = util::parse_uint(optarg); + if (n == -1) { + std::cerr << "-r: bad option value: " << optarg << std::endl; + exit(EXIT_FAILURE); + } + if (n == 0) { + std::cerr << "-r: the rate at which connections are made " + << "must be positive." << std::endl; + exit(EXIT_FAILURE); + } + config.rate = n; + break; + } + case 'T': + config.conn_active_timeout = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.conn_active_timeout)) { + std::cerr << "-T: bad value for the conn_active_timeout wait time: " + << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'N': + config.conn_inactivity_timeout = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.conn_inactivity_timeout)) { + std::cerr << "-N: bad value for the conn_inactivity_timeout wait time: " + << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'B': { + auto arg = StringRef{optarg}; + config.base_uri = ""; + config.base_uri_unix = false; + + if (util::istarts_with_l(arg, UNIX_PATH_PREFIX)) { + // UNIX domain socket path + sockaddr_un un; + + auto path = StringRef{std::begin(arg) + str_size(UNIX_PATH_PREFIX), + std::end(arg)}; + + if (path.size() == 0 || path.size() + 1 > sizeof(un.sun_path)) { + std::cerr << "--base-uri: invalid UNIX domain socket path: " << arg + << std::endl; + exit(EXIT_FAILURE); + } + + config.base_uri_unix = true; + + auto &unix_addr = config.unix_addr; + std::copy(std::begin(path), std::end(path), unix_addr.sun_path); + unix_addr.sun_path[path.size()] = '\0'; + unix_addr.sun_family = AF_UNIX; + + break; + } + + if (!parse_base_uri(arg)) { + std::cerr << "--base-uri: invalid base URI: " << arg << std::endl; + exit(EXIT_FAILURE); + } + + config.base_uri = arg.str(); + break; + } + case 'D': + config.duration = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.duration)) { + std::cerr << "-D: value error " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 'v': + config.verbose = true; + break; + case 'h': + print_help(std::cout); + exit(EXIT_SUCCESS); + case '?': + util::show_candidates(argv[optind - 1], long_options); + exit(EXIT_FAILURE); + case 0: + switch (flag) { + case 1: + // version option + print_version(std::cout); + exit(EXIT_SUCCESS); + case 2: + // ciphers option + config.ciphers = optarg; + break; + case 3: + // timing-script option + config.ifile = optarg; + config.timing_script = true; + break; + case 5: + // rate-period + config.rate_period = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.rate_period)) { + std::cerr << "--rate-period: value error " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 6: + // --h1 + config.alpn_list = + util::parse_config_str_list(StringRef::from_lit("http/1.1")); + config.no_tls_proto = Config::PROTO_HTTP1_1; + break; + case 7: + // --header-table-size + if (parse_header_table_size(config.header_table_size, + "header-table-size", optarg) != 0) { + exit(EXIT_FAILURE); + } + break; + case 8: + // --encoder-header-table-size + if (parse_header_table_size(config.encoder_header_table_size, + "encoder-header-table-size", optarg) != 0) { + exit(EXIT_FAILURE); + } + break; + case 9: + // --warm-up-time + config.warm_up_time = util::parse_duration_with_unit(optarg); + if (!std::isfinite(config.warm_up_time)) { + std::cerr << "--warm-up-time: value error " << optarg << std::endl; + exit(EXIT_FAILURE); + } + break; + case 10: + // --log-file + logfile = optarg; + break; + case 11: { + // --connect-to + auto p = util::split_hostport(StringRef{optarg}); + int64_t port = 0; + if (p.first.empty() || + (!p.second.empty() && (port = util::parse_uint(p.second)) == -1)) { + std::cerr << "--connect-to: Invalid value " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.connect_to_host = p.first.str(); + config.connect_to_port = port; + break; + } + case 12: { + char *end; + auto v = std::strtod(optarg, &end); + if (end == optarg || *end != '\0' || !std::isfinite(v) || + 1. / v < 1e-6) { + std::cerr << "--rps: Invalid value " << optarg << std::endl; + exit(EXIT_FAILURE); + } + config.rps = v; + break; + } + case 13: + // --groups + config.groups = optarg; + break; + case 14: + // --tls13-ciphers + config.tls13_ciphers = optarg; + break; + case 15: + // --no-udp-gso + config.no_udp_gso = true; + break; + case 16: + // --qlog-file-base + config.qlog_file_base = optarg; + break; + case 17: { + // --max-udp-payload-size + auto n = util::parse_uint_with_unit(optarg); + if (n == -1) { + std::cerr << "--max-udp-payload-size: bad option value: " << optarg + << std::endl; + exit(EXIT_FAILURE); + } + if (static_cast<uint64_t>(n) > 64_k) { + std::cerr << "--max-udp-payload-size: must not exceed 65536" + << std::endl; + exit(EXIT_FAILURE); + } + config.max_udp_payload_size = n; + break; + } + case 18: + // --ktls + config.ktls = true; + break; + case 4: + // npn-list option + std::cerr << "--npn-list: deprecated. Use --alpn-list instead." + << std::endl; + // fall through + case 19: + // alpn-list option + config.alpn_list = util::parse_config_str_list(StringRef{optarg}); + break; + } + break; + default: + break; + } + } + + if (argc == optind) { + if (config.ifile.empty()) { + std::cerr << "no URI or input file given" << std::endl; + exit(EXIT_FAILURE); + } + } + + if (config.nclients == 0) { + std::cerr << "-c: the number of clients must be strictly greater than 0." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.alpn_list.empty()) { + config.alpn_list = + util::parse_config_str_list(StringRef::from_lit(DEFAULT_ALPN_LIST)); + } + + // serialize the APLN tokens + for (auto &proto : config.alpn_list) { + proto.insert(proto.begin(), static_cast<unsigned char>(proto.size())); + } + + std::vector<std::string> reqlines; + + if (config.ifile.empty()) { + std::vector<std::string> uris; + std::copy(&argv[optind], &argv[argc], std::back_inserter(uris)); + reqlines = parse_uris(std::begin(uris), std::end(uris)); + } else { + std::vector<std::string> uris; + if (!config.timing_script) { + if (config.ifile == "-") { + uris = read_uri_from_file(std::cin); + } else { + std::ifstream infile(config.ifile); + if (!infile) { + std::cerr << "cannot read input file: " << config.ifile << std::endl; + exit(EXIT_FAILURE); + } + + uris = read_uri_from_file(infile); + } + } else { + if (config.ifile == "-") { + read_script_from_file(std::cin, config.timings, uris); + } else { + std::ifstream infile(config.ifile); + if (!infile) { + std::cerr << "cannot read input file: " << config.ifile << std::endl; + exit(EXIT_FAILURE); + } + + read_script_from_file(infile, config.timings, uris); + } + + if (nreqs_set_manually) { + if (config.nreqs > uris.size()) { + std::cerr << "-n: the number of requests must be less than or equal " + "to the number of timing script entries. Setting number " + "of requests to " + << uris.size() << std::endl; + + config.nreqs = uris.size(); + } + } else { + config.nreqs = uris.size(); + } + } + + reqlines = parse_uris(std::begin(uris), std::end(uris)); + } + + if (reqlines.empty()) { + std::cerr << "No URI given" << std::endl; + exit(EXIT_FAILURE); + } + + if (config.is_timing_based_mode() && config.is_rate_mode()) { + std::cerr << "-r, -D: they are mutually exclusive." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.timing_script && config.rps_enabled()) { + std::cerr << "--timing-script-file, --rps: they are mutually exclusive." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nreqs == 0 && !config.is_timing_based_mode()) { + std::cerr << "-n: the number of requests must be strictly greater than 0 " + "if timing-based test is not being run." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.max_concurrent_streams == 0) { + std::cerr << "-m: the max concurrent streams must be strictly greater " + << "than 0." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nthreads == 0) { + std::cerr << "-t: the number of threads must be strictly greater than 0." + << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nthreads > std::thread::hardware_concurrency()) { + std::cerr << "-t: warning: the number of threads is greater than hardware " + << "cores." << std::endl; + } + + // With timing script, we don't distribute config.nreqs to each + // client or thread. + if (!config.timing_script && config.nreqs < config.nclients && + !config.is_timing_based_mode()) { + std::cerr << "-n, -c: the number of requests must be greater than or " + << "equal to the clients." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.nclients < config.nthreads) { + std::cerr << "-c, -t: the number of clients must be greater than or equal " + << "to the number of threads." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.is_timing_based_mode()) { + config.nreqs = 0; + } + + if (config.is_rate_mode()) { + if (config.rate < config.nthreads) { + std::cerr << "-r, -t: the connection rate must be greater than or equal " + << "to the number of threads." << std::endl; + exit(EXIT_FAILURE); + } + + if (config.rate > config.nclients) { + std::cerr << "-r, -c: the connection rate must be smaller than or equal " + "to the number of clients." + << std::endl; + exit(EXIT_FAILURE); + } + } + + if (!datafile.empty()) { + config.data_fd = open(datafile.c_str(), O_RDONLY | O_BINARY); + if (config.data_fd == -1) { + std::cerr << "-d: Could not open file " << datafile << std::endl; + exit(EXIT_FAILURE); + } + struct stat data_stat; + if (fstat(config.data_fd, &data_stat) == -1) { + std::cerr << "-d: Could not stat file " << datafile << std::endl; + exit(EXIT_FAILURE); + } + config.data_length = data_stat.st_size; + auto addr = mmap(nullptr, config.data_length, PROT_READ, MAP_SHARED, + config.data_fd, 0); + if (addr == MAP_FAILED) { + std::cerr << "-d: Could not mmap file " << datafile << std::endl; + exit(EXIT_FAILURE); + } + config.data = static_cast<uint8_t *>(addr); + } + + if (!logfile.empty()) { + config.log_fd = open(logfile.c_str(), O_WRONLY | O_CREAT | O_APPEND, + S_IRUSR | S_IWUSR | S_IRGRP); + if (config.log_fd == -1) { + std::cerr << "--log-file: Could not open file " << logfile << std::endl; + exit(EXIT_FAILURE); + } + } + + if (!config.qlog_file_base.empty() && !config.is_quic()) { + std::cerr << "Warning: --qlog-file-base: only effective in quic, ignoring." + << std::endl; + } + + struct sigaction act {}; + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, nullptr); + + auto ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!ssl_ctx) { + std::cerr << "Failed to create SSL_CTX: " + << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + exit(EXIT_FAILURE); + } + + auto ssl_opts = (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION; + +#ifdef SSL_OP_ENABLE_KTLS + if (config.ktls) { + ssl_opts |= SSL_OP_ENABLE_KTLS; + } +#endif // SSL_OP_ENABLE_KTLS + + SSL_CTX_set_options(ssl_ctx, ssl_opts); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS); + + if (config.is_quic()) { +#ifdef ENABLE_HTTP3 +# ifdef HAVE_LIBNGTCP2_CRYPTO_QUICTLS + if (ngtcp2_crypto_quictls_configure_client_context(ssl_ctx) != 0) { + std::cerr << "ngtcp2_crypto_quictls_configure_client_context failed" + << std::endl; + exit(EXIT_FAILURE); + } +# endif // HAVE_LIBNGTCP2_CRYPTO_QUICTLS +# ifdef HAVE_LIBNGTCP2_CRYPTO_BORINGSSL + if (ngtcp2_crypto_boringssl_configure_client_context(ssl_ctx) != 0) { + std::cerr << "ngtcp2_crypto_boringssl_configure_client_context failed" + << std::endl; + exit(EXIT_FAILURE); + } +# endif // HAVE_LIBNGTCP2_CRYPTO_BORINGSSL +#endif // ENABLE_HTTP3 + } else if (nghttp2::tls::ssl_ctx_set_proto_versions( + ssl_ctx, nghttp2::tls::NGHTTP2_TLS_MIN_VERSION, + nghttp2::tls::NGHTTP2_TLS_MAX_VERSION) != 0) { + std::cerr << "Could not set TLS versions" << std::endl; + exit(EXIT_FAILURE); + } + + if (SSL_CTX_set_cipher_list(ssl_ctx, config.ciphers.c_str()) == 0) { + std::cerr << "SSL_CTX_set_cipher_list with " << config.ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + exit(EXIT_FAILURE); + } + +#if defined(NGHTTP2_GENUINE_OPENSSL) || defined(NGHTTP2_OPENSSL_IS_LIBRESSL) + if (SSL_CTX_set_ciphersuites(ssl_ctx, config.tls13_ciphers.c_str()) == 0) { + std::cerr << "SSL_CTX_set_ciphersuites with " << config.tls13_ciphers + << " failed: " << ERR_error_string(ERR_get_error(), nullptr) + << std::endl; + exit(EXIT_FAILURE); + } +#endif // NGHTTP2_GENUINE_OPENSSL || NGHTTP2_OPENSSL_IS_LIBRESSL + + if (SSL_CTX_set1_groups_list(ssl_ctx, config.groups.c_str()) != 1) { + std::cerr << "SSL_CTX_set1_groups_list failed" << std::endl; + exit(EXIT_FAILURE); + } + + std::vector<unsigned char> proto_list; + for (const auto &proto : config.alpn_list) { + std::copy_n(proto.c_str(), proto.size(), std::back_inserter(proto_list)); + } + + SSL_CTX_set_alpn_protos(ssl_ctx, proto_list.data(), proto_list.size()); + + auto keylog_filename = getenv("SSLKEYLOGFILE"); + if (keylog_filename) { + keylog_file.open(keylog_filename, std::ios_base::app); + if (keylog_file) { + SSL_CTX_set_keylog_callback(ssl_ctx, keylog_callback); + } + } + + std::string user_agent = "h2load nghttp2/" NGHTTP2_VERSION; + Headers shared_nva; + shared_nva.emplace_back(":scheme", config.scheme); + shared_nva.emplace_back(":authority", make_http_authority(config)); + shared_nva.emplace_back(":method", config.data_fd == -1 ? "GET" : "POST"); + shared_nva.emplace_back("user-agent", user_agent); + + // list header fields that can be overridden. + auto override_hdrs = make_array<std::string>(":authority", ":host", ":method", + ":scheme", "user-agent"); + + for (auto &kv : config.custom_headers) { + if (std::find(std::begin(override_hdrs), std::end(override_hdrs), + kv.name) != std::end(override_hdrs)) { + // override header + for (auto &nv : shared_nva) { + if ((nv.name == ":authority" && kv.name == ":host") || + (nv.name == kv.name)) { + nv.value = kv.value; + } + } + } else { + // add additional headers + shared_nva.push_back(kv); + } + } + + std::string content_length_str; + if (config.data_fd != -1) { + content_length_str = util::utos(config.data_length); + } + + auto method_it = + std::find_if(std::begin(shared_nva), std::end(shared_nva), + [](const Header &nv) { return nv.name == ":method"; }); + assert(method_it != std::end(shared_nva)); + + config.h1reqs.reserve(reqlines.size()); + config.nva.reserve(reqlines.size()); + + for (auto &req : reqlines) { + // For HTTP/1.1 + auto h1req = (*method_it).value; + h1req += ' '; + h1req += req; + h1req += " HTTP/1.1\r\n"; + for (auto &nv : shared_nva) { + if (nv.name == ":authority") { + h1req += "Host: "; + h1req += nv.value; + h1req += "\r\n"; + continue; + } + if (nv.name[0] == ':') { + continue; + } + h1req += nv.name; + h1req += ": "; + h1req += nv.value; + h1req += "\r\n"; + } + + if (!content_length_str.empty()) { + h1req += "Content-Length: "; + h1req += content_length_str; + h1req += "\r\n"; + } + h1req += "\r\n"; + + config.h1reqs.push_back(std::move(h1req)); + + // For nghttp2 + std::vector<nghttp2_nv> nva; + // 2 for :path, and possible content-length + nva.reserve(2 + shared_nva.size()); + + nva.push_back(http2::make_nv_ls(":path", req)); + + for (auto &nv : shared_nva) { + nva.push_back(http2::make_nv(nv.name, nv.value, false)); + } + + if (!content_length_str.empty()) { + nva.push_back(http2::make_nv(StringRef::from_lit("content-length"), + StringRef{content_length_str})); + } + + config.nva.push_back(std::move(nva)); + } + + // Don't DOS our server! + if (config.host == "nghttp2.org") { + std::cerr << "Using h2load against public server " << config.host + << " should be prohibited." << std::endl; + exit(EXIT_FAILURE); + } + + resolve_host(); + + std::cout << "starting benchmark..." << std::endl; + + std::vector<std::unique_ptr<Worker>> workers; + workers.reserve(config.nthreads); + +#ifndef NOTHREADS + size_t nreqs_per_thread = 0; + ssize_t nreqs_rem = 0; + + if (!config.timing_script) { + nreqs_per_thread = config.nreqs / config.nthreads; + nreqs_rem = config.nreqs % config.nthreads; + } + + size_t nclients_per_thread = config.nclients / config.nthreads; + ssize_t nclients_rem = config.nclients % config.nthreads; + + size_t rate_per_thread = config.rate / config.nthreads; + ssize_t rate_per_thread_rem = config.rate % config.nthreads; + + size_t max_samples_per_thread = + std::max(static_cast<size_t>(256), MAX_SAMPLES / config.nthreads); + + std::mutex mu; + std::condition_variable cv; + auto ready = false; + + std::vector<std::future<void>> futures; + for (size_t i = 0; i < config.nthreads; ++i) { + auto rate = rate_per_thread; + if (rate_per_thread_rem > 0) { + --rate_per_thread_rem; + ++rate; + } + auto nclients = nclients_per_thread; + if (nclients_rem > 0) { + --nclients_rem; + ++nclients; + } + + size_t nreqs; + if (config.timing_script) { + // With timing script, each client issues config.nreqs requests. + // We divide nreqs by number of clients in Worker ctor to + // distribute requests to those clients evenly, so multiply + // config.nreqs here by config.nclients. + nreqs = config.nreqs * nclients; + } else { + nreqs = nreqs_per_thread; + if (nreqs_rem > 0) { + --nreqs_rem; + ++nreqs; + } + } + + workers.push_back(create_worker(i, ssl_ctx, nreqs, nclients, rate, + max_samples_per_thread)); + auto &worker = workers.back(); + futures.push_back( + std::async(std::launch::async, [&worker, &mu, &cv, &ready]() { + { + std::unique_lock<std::mutex> ulk(mu); + cv.wait(ulk, [&ready] { return ready; }); + } + worker->run(); + })); + } + + { + std::lock_guard<std::mutex> lg(mu); + ready = true; + cv.notify_all(); + } + + auto start = std::chrono::steady_clock::now(); + + for (auto &fut : futures) { + fut.get(); + } + +#else // NOTHREADS + auto rate = config.rate; + auto nclients = config.nclients; + auto nreqs = + config.timing_script ? config.nreqs * config.nclients : config.nreqs; + + workers.push_back( + create_worker(0, ssl_ctx, nreqs, nclients, rate, MAX_SAMPLES)); + + auto start = std::chrono::steady_clock::now(); + + workers.back()->run(); +#endif // NOTHREADS + + auto end = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::microseconds>(end - start); + + Stats stats(0, 0); + for (const auto &w : workers) { + const auto &s = w->stats; + + stats.req_todo += s.req_todo; + stats.req_started += s.req_started; + stats.req_done += s.req_done; + stats.req_timedout += s.req_timedout; + stats.req_success += s.req_success; + stats.req_status_success += s.req_status_success; + stats.req_failed += s.req_failed; + stats.req_error += s.req_error; + stats.bytes_total += s.bytes_total; + stats.bytes_head += s.bytes_head; + stats.bytes_head_decomp += s.bytes_head_decomp; + stats.bytes_body += s.bytes_body; + stats.udp_dgram_recv += s.udp_dgram_recv; + stats.udp_dgram_sent += s.udp_dgram_sent; + + for (size_t i = 0; i < stats.status.size(); ++i) { + stats.status[i] += s.status[i]; + } + } + + auto ts = process_time_stats(workers); + + // Requests which have not been issued due to connection errors, are + // counted towards req_failed and req_error. + auto req_not_issued = + (stats.req_todo - stats.req_status_success - stats.req_failed); + stats.req_failed += req_not_issued; + stats.req_error += req_not_issued; + + // UI is heavily inspired by weighttp[1] and wrk[2] + // + // [1] https://github.com/lighttpd/weighttp + // [2] https://github.com/wg/wrk + double rps = 0; + int64_t bps = 0; + if (duration.count() > 0) { + if (config.is_timing_based_mode()) { + // we only want to consider the main duration if warm-up is given + rps = stats.req_success / config.duration; + bps = stats.bytes_total / config.duration; + } else { + auto secd = std::chrono::duration_cast< + std::chrono::duration<double, std::chrono::seconds::period>>( + duration); + rps = stats.req_success / secd.count(); + bps = stats.bytes_total / secd.count(); + } + } + + double header_space_savings = 0.; + if (stats.bytes_head_decomp > 0) { + header_space_savings = + 1. - static_cast<double>(stats.bytes_head) / stats.bytes_head_decomp; + } + + std::cout << std::fixed << std::setprecision(2) << R"( +finished in )" + << util::format_duration(duration) << ", " << rps << " req/s, " + << util::utos_funit(bps) << R"(B/s +requests: )" << stats.req_todo + << " total, " << stats.req_started << " started, " << stats.req_done + << " done, " << stats.req_status_success << " succeeded, " + << stats.req_failed << " failed, " << stats.req_error + << " errored, " << stats.req_timedout << R"( timeout +status codes: )" + << stats.status[2] << " 2xx, " << stats.status[3] << " 3xx, " + << stats.status[4] << " 4xx, " << stats.status[5] << R"( 5xx +traffic: )" << util::utos_funit(stats.bytes_total) + << "B (" << stats.bytes_total << ") total, " + << util::utos_funit(stats.bytes_head) << "B (" << stats.bytes_head + << ") headers (space savings " << header_space_savings * 100 + << "%), " << util::utos_funit(stats.bytes_body) << "B (" + << stats.bytes_body << R"() data)" << std::endl; +#ifdef ENABLE_HTTP3 + if (config.is_quic()) { + std::cout << "UDP datagram: " << stats.udp_dgram_sent << " sent, " + << stats.udp_dgram_recv << " received" << std::endl; + } +#endif // ENABLE_HTTP3 + std::cout + << R"( min max mean sd +/- sd +time for request: )" + << std::setw(10) << util::format_duration(ts.request.min) << " " + << std::setw(10) << util::format_duration(ts.request.max) << " " + << std::setw(10) << util::format_duration(ts.request.mean) << " " + << std::setw(10) << util::format_duration(ts.request.sd) << std::setw(9) + << util::dtos(ts.request.within_sd) << "%" + << "\ntime for connect: " << std::setw(10) + << util::format_duration(ts.connect.min) << " " << std::setw(10) + << util::format_duration(ts.connect.max) << " " << std::setw(10) + << util::format_duration(ts.connect.mean) << " " << std::setw(10) + << util::format_duration(ts.connect.sd) << std::setw(9) + << util::dtos(ts.connect.within_sd) << "%" + << "\ntime to 1st byte: " << std::setw(10) + << util::format_duration(ts.ttfb.min) << " " << std::setw(10) + << util::format_duration(ts.ttfb.max) << " " << std::setw(10) + << util::format_duration(ts.ttfb.mean) << " " << std::setw(10) + << util::format_duration(ts.ttfb.sd) << std::setw(9) + << util::dtos(ts.ttfb.within_sd) << "%" + << "\nreq/s : " << std::setw(10) << ts.rps.min << " " + << std::setw(10) << ts.rps.max << " " << std::setw(10) << ts.rps.mean + << " " << std::setw(10) << ts.rps.sd << std::setw(9) + << util::dtos(ts.rps.within_sd) << "%" << std::endl; + + SSL_CTX_free(ssl_ctx); + + if (config.log_fd != -1) { + close(config.log_fd); + } + + return 0; +} + +} // namespace h2load + +int main(int argc, char **argv) { return h2load::main(argc, argv); } |