From 8ecb980d26f48c46463c4cebbfd3b043acce9ce9 Mon Sep 17 00:00:00 2001 From: Dmitri Tikhonov Date: Thu, 18 Feb 2021 10:09:35 -0500 Subject: [PATCH] Release 2.29.1 - Make it possible to build the library and unit tests without libevent. - Build all command-line utilities in bin/ - Add perf_client and perf_server command-line utilities to test performance according to the "perf" protocol. --- CHANGELOG | 8 + bin/CMakeLists.txt | 4 + bin/perf_client.c | 406 +++++++++++++++++++++++++++++++++++++++++++++ bin/perf_server.c | 253 ++++++++++++++++++++++++++++ docs/conf.py | 2 +- include/lsquic.h | 2 +- 6 files changed, 673 insertions(+), 2 deletions(-) create mode 100644 bin/perf_client.c create mode 100644 bin/perf_server.c diff --git a/CHANGELOG b/CHANGELOG index 12db49170..9f4c7ef61 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +2021-02-18 + - 2.29.1 + - Make it possible to build the library and unit tests without + libevent. + - Build all command-line utilities in bin/ + - Add perf_client and perf_server command-line utilities to test + performance according to the "perf" protocol. + 2021-02-10 - 2.29.0 - [FEATURE] QUIC and HTTP/3 Internet Draft 34 support and v1 support. diff --git a/bin/CMakeLists.txt b/bin/CMakeLists.txt index 577432a8c..4af9afa90 100644 --- a/bin/CMakeLists.txt +++ b/bin/CMakeLists.txt @@ -32,6 +32,8 @@ add_executable(echo_server echo_server.c prog.c test_common.c test_cert.c ${GETO add_executable(echo_client echo_client.c prog.c test_common.c test_cert.c ${GETOPT_C}) add_executable(duck_server duck_server.c prog.c test_common.c test_cert.c ${GETOPT_C}) add_executable(duck_client duck_client.c prog.c test_common.c test_cert.c ${GETOPT_C}) +add_executable(perf_client perf_client.c prog.c test_common.c test_cert.c ${GETOPT_C}) +add_executable(perf_server perf_server.c prog.c test_common.c test_cert.c ${GETOPT_C}) IF (NOT MSVC) @@ -67,6 +69,8 @@ TARGET_LINK_LIBRARIES(echo_server ${LIBS}) TARGET_LINK_LIBRARIES(echo_client ${LIBS}) TARGET_LINK_LIBRARIES(duck_server ${LIBS}) TARGET_LINK_LIBRARIES(duck_client ${LIBS}) +TARGET_LINK_LIBRARIES(perf_client ${LIBS}) +TARGET_LINK_LIBRARIES(perf_server ${LIBS}) INCLUDE(CheckFunctionExists) diff --git a/bin/perf_client.c b/bin/perf_client.c new file mode 100644 index 000000000..7fa947d96 --- /dev/null +++ b/bin/perf_client.c @@ -0,0 +1,406 @@ +/* Copyright (c) 2017 - 2021 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * perf_client.c -- Implements the "perf" client, see + * https://tools.ietf.org/html/draft-banks-quic-performance-00 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#endif + +#include + +#include "lsquic.h" +#include "test_common.h" +#include "prog.h" + +#include "../src/liblsquic/lsquic_logger.h" +#include "../src/liblsquic/lsquic_int_types.h" +#include "../src/liblsquic/lsquic_byteswap.h" + +struct scenario +{ + STAILQ_ENTRY(scenario) next; + uint64_t bytes_to_request; + uint64_t bytes_to_send; /* After the 8-byte header */ +}; + +/* Assume all connections use the same list of scenarios, so store it in + * a global variable. + */ +static STAILQ_HEAD(, scenario) s_scenarios + = STAILQ_HEAD_INITIALIZER(s_scenarios); +static unsigned s_n_scenarios; +static unsigned s_n_conns; + +struct prog s_prog; + +struct lsquic_conn_ctx +{ + /* Once a connection runs out of scenarios, no new streams are created + * and the connection is closed when all streams are closed. + */ + const struct scenario *next_scenario; + unsigned n_scenarios_left; + unsigned n_streams; +}; + + +static bool +perf_create_streams (struct lsquic_conn *conn, struct lsquic_conn_ctx *conn_ctx) +{ + if (conn_ctx->n_scenarios_left) + { + --conn_ctx->n_scenarios_left; + lsquic_conn_make_stream(conn); + return true; + } + else + return false; +} + + +static lsquic_conn_ctx_t * +perf_client_on_new_conn (void *stream_if_ctx, struct lsquic_conn *conn) +{ + struct lsquic_conn_ctx *conn_ctx; + + if (s_n_scenarios) + { + conn_ctx = calloc(1, sizeof(*conn_ctx)); + conn_ctx->next_scenario = STAILQ_FIRST(&s_scenarios); + conn_ctx->n_scenarios_left = s_n_scenarios; + perf_create_streams(conn, conn_ctx); + ++s_n_conns; + return conn_ctx; + } + else + { + lsquic_conn_close(conn); + return NULL; + } +} + + +static void +perf_client_on_conn_closed (struct lsquic_conn *conn) +{ + struct lsquic_conn_ctx *conn_ctx; + + LSQ_NOTICE("Connection closed"); + conn_ctx = lsquic_conn_get_ctx(conn); + free(conn_ctx); + --s_n_conns; + if (0 == s_n_conns) + prog_stop(&s_prog); +} + + +struct lsquic_stream_ctx +{ + const struct scenario *scenario; + struct { + uint64_t header; /* Big-endian */ + unsigned n_h; /* Number of header bytes written */ + uint64_t n_written; /* Number of non-header bytes written */ + } write_state; + struct { + uint64_t n_read; + } read_state; +}; + + +static struct lsquic_stream_ctx * +perf_client_on_new_stream (void *stream_if_ctx, lsquic_stream_t *stream) +{ + struct lsquic_conn_ctx *conn_ctx; + struct lsquic_conn *conn; + + conn = lsquic_stream_conn(stream); + conn_ctx = lsquic_conn_get_ctx(conn); + + if (!stream) + { + LSQ_NOTICE("%s: got null stream: no more streams possible", __func__); + lsquic_conn_close(conn); + return NULL; + } + + assert(conn_ctx->next_scenario); + + struct lsquic_stream_ctx *stream_ctx = calloc(1, sizeof(*stream_ctx)); + stream_ctx->scenario = conn_ctx->next_scenario; + conn_ctx->next_scenario = STAILQ_NEXT(conn_ctx->next_scenario, next); +#if __BYTE_ORDER == __LITTLE_ENDIAN + stream_ctx->write_state.header + = bswap_64(stream_ctx->scenario->bytes_to_request); +#else + stream_ctx->write_state.header = stream_ctx->scenario->bytes_to_request; +#endif + lsquic_stream_wantwrite(stream, 1); + return stream_ctx; +} + + +static size_t +buffer_size (void *lsqr_ctx) +{ + struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx; + return stream_ctx->scenario->bytes_to_send + - stream_ctx->write_state.n_written; +} + + +static size_t +buffer_read (void *lsqr_ctx, void *buf, size_t count) +{ + struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx; + size_t left; + + left = buffer_size(stream_ctx); + if (count > left) + count = left; + memset(buf, 0, count); + stream_ctx->write_state.n_written += count; + return count; +} + + +static size_t +header_size (void *lsqr_ctx) +{ + struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx; + return sizeof(uint64_t) - stream_ctx->write_state.n_h; +} + + +static size_t +header_read (void *lsqr_ctx, void *buf, size_t count) +{ + struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx; + const unsigned char *src; + size_t left; + + left = header_size(stream_ctx); + if (count < left) + count = left; + src = (unsigned char *) &stream_ctx->write_state.header + + sizeof(uint64_t) - left; + memcpy(buf, src, count); + stream_ctx->write_state.n_h += count; + return count; +} + + +static void +perf_client_on_write (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + struct lsquic_reader reader; + ssize_t nw; + + if (stream_ctx->write_state.n_h >= sizeof(uint64_t)) + reader = (struct lsquic_reader) { + buffer_read, + buffer_size, + stream_ctx, + }; + else + reader = (struct lsquic_reader) { + header_read, + header_size, + stream_ctx, + }; + + nw = lsquic_stream_writef(stream, &reader); + if (nw >= 0) + LSQ_DEBUG("%s: wrote %zd bytes", __func__, nw); + else + LSQ_WARN("%s: cannot write to stream: %s", __func__, strerror(errno)); + + if (reader.lsqr_size(stream_ctx) == 0 + && (reader.lsqr_size == buffer_size || buffer_size(stream_ctx) == 0)) + { + lsquic_stream_shutdown(stream, 1); + lsquic_stream_wantread(stream, 1); + } +} + + +static size_t +perf_read_and_discard (void *user_data, const unsigned char *buf, + size_t count, int fin) +{ + return count; +} + + +static void +perf_client_on_read (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + ssize_t nr; + + nr = lsquic_stream_readf(stream, perf_read_and_discard, NULL); + if (nr >= 0) + { + stream_ctx->read_state.n_read += nr; + if (nr == 0) + { + LSQ_DEBUG("reached fin after reading %"PRIu64" bytes from server", + stream_ctx->read_state.n_read); + lsquic_stream_shutdown(stream, 0); + } + } + else + { + LSQ_WARN("error reading from stream: %s, abort connection", + strerror(errno)); + lsquic_stream_close(stream); + lsquic_conn_abort(lsquic_stream_conn(stream)); + } +} + + +static void +perf_client_on_close (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + struct lsquic_conn_ctx *conn_ctx; + struct lsquic_conn *conn; + + conn = lsquic_stream_conn(stream); + conn_ctx = lsquic_conn_get_ctx(conn); + if (!perf_create_streams(conn, conn_ctx)) + { + LSQ_DEBUG("out of scenarios, will close connection"); + lsquic_conn_close(conn); + } + free(stream_ctx); +} + + +const struct lsquic_stream_if perf_stream_if = { + .on_new_conn = perf_client_on_new_conn, + .on_conn_closed = perf_client_on_conn_closed, + .on_new_stream = perf_client_on_new_stream, + .on_read = perf_client_on_read, + .on_write = perf_client_on_write, + .on_close = perf_client_on_close, +}; + + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + printf( +"Usage: %s [opts]\n" +"\n" +"Options:\n" +" -p NREQ:NSEND Request NREQ bytes from server and, in addition, send\n" +" NSEND bytes to server. May be specified many times\n" +" and must be specified at least once.\n" +" -T FILE Print stats to FILE. If FILE is -, print stats to stdout.\n" + , prog); +} + + +int +main (int argc, char **argv) +{ + char *p; + int opt, s; + struct sport_head sports; + struct scenario *scenario; + + TAILQ_INIT(&sports); + prog_init(&s_prog, 0, &sports, &perf_stream_if, NULL); + s_prog.prog_api.ea_alpn = "perf"; + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "hp:T:"))) + { + switch (opt) { + case 'p': + scenario = calloc(1, sizeof(*scenario)); + if (!scenario) + { + perror("calloc"); + exit(EXIT_FAILURE); + } + ++s_n_scenarios; + STAILQ_INSERT_TAIL(&s_scenarios, scenario, next); + scenario->bytes_to_request = strtoull(optarg, &p, 10); + if (*p != ':') + { + fprintf(stderr, "invalid scenario `%s'\n", optarg); + exit(EXIT_FAILURE); + } + scenario->bytes_to_send = strtoull(p + 1, NULL, 10); + break; + case 'T': + if (0 == strcmp(optarg, "-")) + s_prog.prog_api.ea_stats_fh = stdout; + else + { + s_prog.prog_api.ea_stats_fh = fopen(optarg, "w"); + if (!s_prog.prog_api.ea_stats_fh) + { + perror("fopen"); + exit(1); + } + } + break; + case 'h': + usage(argv[0]); + prog_print_common_options(&s_prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&s_prog, opt, optarg)) + exit(1); + } + } + + if (STAILQ_EMPTY(&s_scenarios)) + { + fprintf(stderr, "please specify one of more requests using -p\n"); + exit(1); + } + + if (0 != prog_prep(&s_prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + + if (0 != prog_connect(&s_prog, NULL, 0)) + { + LSQ_ERROR("could not connect"); + exit(EXIT_FAILURE); + } + + LSQ_DEBUG("entering event loop"); + + s = prog_run(&s_prog); + prog_cleanup(&s_prog); + + exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/bin/perf_server.c b/bin/perf_server.c new file mode 100644 index 000000000..0ecebe101 --- /dev/null +++ b/bin/perf_server.c @@ -0,0 +1,253 @@ +/* Copyright (c) 2017 - 2021 LiteSpeed Technologies Inc. See LICENSE. */ +/* + * perf_server.c -- Implements the "perf" server, see + * https://tools.ietf.org/html/draft-banks-quic-performance-00 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WIN32 +#include +#include +#else +#include "vc_compat.h" +#include "getopt.h" +#endif + +#include + +#include "lsquic.h" +#include "test_common.h" +#include "../src/liblsquic/lsquic_hash.h" +#include "test_cert.h" +#include "prog.h" + +#include "../src/liblsquic/lsquic_byteswap.h" +#include "../src/liblsquic/lsquic_logger.h" + + +static lsquic_conn_ctx_t * +perf_server_on_new_conn (void *stream_if_ctx, lsquic_conn_t *conn) +{ + LSQ_INFO("New connection!"); + return NULL; +} + + +static void +perf_server_on_conn_closed (lsquic_conn_t *conn) +{ + LSQ_INFO("Connection closed"); +} + + +struct lsquic_stream_ctx +{ + union { + uint64_t left; /* Number of bytes left to write */ + unsigned char buf[sizeof(uint64_t)]; /* Read client header in */ + } u; + unsigned n_h_read; /* Number of header bytes read in */ +}; + + +static struct lsquic_stream_ctx * +perf_server_on_new_stream (void *unused, struct lsquic_stream *stream) +{ + struct lsquic_stream_ctx *stream_ctx; + + stream_ctx = calloc(1, sizeof(*stream_ctx)); + if (stream_ctx) + { + lsquic_stream_wantread(stream, 1); + return stream_ctx; + } + else + { + perror("calloc"); + exit(EXIT_FAILURE); + } +} + + +static size_t +perf_read_and_discard (void *user_data, const unsigned char *buf, + size_t count, int fin) +{ + return count; +} + + +static void +perf_server_on_read (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + ssize_t nr; + size_t toread; + + if (stream_ctx->n_h_read < sizeof(stream_ctx->u.buf)) + { + /* Read the header */ + toread = sizeof(stream_ctx->u.buf) - stream_ctx->n_h_read; + nr = lsquic_stream_read(stream, stream_ctx->u.buf + + sizeof(stream_ctx->u.buf) - toread, toread); + if (nr > 0) + { + stream_ctx->n_h_read += nr; + if (stream_ctx->n_h_read == sizeof(stream_ctx->u.left)) + { +#if __BYTE_ORDER == __LITTLE_ENDIAN + stream_ctx->u.left = bswap_64(stream_ctx->u.left); +#endif + LSQ_INFO("client requests %"PRIu64" bytes on stream %"PRIu64, + stream_ctx->u.left, lsquic_stream_id(stream)); + } + } + else if (nr < 0) + { + LSQ_WARN("error reading from stream: %s", strerror(errno)); + lsquic_stream_close(stream); + } + else + { + LSQ_WARN("incomplete header on stream %"PRIu64", abort connection", + lsquic_stream_id(stream)); + lsquic_stream_wantread(stream, 0); + lsquic_conn_abort(lsquic_stream_conn(stream)); + } + } + else + { + /* Read up until FIN, discarding whatever the client is sending */ + nr = lsquic_stream_readf(stream, perf_read_and_discard, NULL); + if (nr == 0) + { + lsquic_stream_wantread(stream, 0); + lsquic_stream_wantwrite(stream, 1); + } + else if (nr < 0) + { + LSQ_WARN("error reading from stream: %s", strerror(errno)); + lsquic_stream_close(stream); + } + } +} + + +static size_t +buffer_size (void *lsqr_ctx) +{ + struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx; + return stream_ctx->u.left; +} + + +static size_t +buffer_read (void *lsqr_ctx, void *buf, size_t count) +{ + struct lsquic_stream_ctx *const stream_ctx = lsqr_ctx; + size_t left; + + left = buffer_size(stream_ctx); + if (count > left) + count = left; + memset(buf, 0, count); + stream_ctx->u.left -= count; + return count; +} + + +static void +perf_server_on_write (struct lsquic_stream *stream, + struct lsquic_stream_ctx *stream_ctx) +{ + struct lsquic_reader reader; + ssize_t nw; + + reader = (struct lsquic_reader) { buffer_read, buffer_size, stream_ctx, }; + nw = lsquic_stream_writef(stream, &reader); + if (nw >= 0) + LSQ_DEBUG("%s: wrote %zd bytes", __func__, nw); + else + LSQ_WARN("%s: cannot write to stream: %s", __func__, strerror(errno)); + + if (stream_ctx->u.left == 0) + lsquic_stream_shutdown(stream, 1); +} + + +static void +perf_server_on_close (lsquic_stream_t *stream, lsquic_stream_ctx_t *stream_ctx) +{ + LSQ_DEBUG("stream closed"); + free(stream_ctx); +} + + +const struct lsquic_stream_if perf_server_stream_if = { + .on_new_conn = perf_server_on_new_conn, + .on_conn_closed = perf_server_on_conn_closed, + .on_new_stream = perf_server_on_new_stream, + .on_read = perf_server_on_read, + .on_write = perf_server_on_write, + .on_close = perf_server_on_close, +}; + + +static void +usage (const char *prog) +{ + const char *const slash = strrchr(prog, '/'); + if (slash) + prog = slash + 1; + printf( +"Usage: %s [opts]\n" +"\n" + , prog); +} + + +int +main (int argc, char **argv) +{ + int opt, s; + struct prog prog; + struct sport_head sports; + + TAILQ_INIT(&sports); + prog_init(&prog, LSENG_SERVER, &sports, &perf_server_stream_if, NULL); + + while (-1 != (opt = getopt(argc, argv, PROG_OPTS "h"))) + { + switch (opt) { + case 'h': + usage(argv[0]); + prog_print_common_options(&prog, stdout); + exit(0); + default: + if (0 != prog_set_opt(&prog, opt, optarg)) + exit(1); + } + } + + add_alpn("perf"); + if (0 != prog_prep(&prog)) + { + LSQ_ERROR("could not prep"); + exit(EXIT_FAILURE); + } + + LSQ_DEBUG("entering event loop"); + + s = prog_run(&prog); + prog_cleanup(&prog); + + exit(0 == s ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/docs/conf.py b/docs/conf.py index d23fddf8d..d8dd481be 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # The short X.Y version version = u'2.29' # The full version, including alpha/beta/rc tags -release = u'2.29.0' +release = u'2.29.1' # -- General configuration --------------------------------------------------- diff --git a/include/lsquic.h b/include/lsquic.h index 529d85e75..d729a91d9 100644 --- a/include/lsquic.h +++ b/include/lsquic.h @@ -25,7 +25,7 @@ extern "C" { #define LSQUIC_MAJOR_VERSION 2 #define LSQUIC_MINOR_VERSION 29 -#define LSQUIC_PATCH_VERSION 0 +#define LSQUIC_PATCH_VERSION 1 /** * Engine flags: