/*
 * Copyright 2019-2021 OARC, Inc.
 * Copyright 2017-2018 Akamai Technologies
 * Copyright 2006-2016 Nominum, Inc.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/***
 ***	DNS Resolution Performance Testing Tool
 ***/

#include "config.h"

#include "datafile.h"
#include "dns.h"
#include "log.h"
#include "net.h"
#include "opt.h"
#include "util.h"
#include "os.h"
#include "list.h"
#include "result.h"
#include "buffer.h"

#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <openssl/ssl.h>
#include <openssl/conf.h>
#include <openssl/err.h>
#include <signal.h>

/*
 * Global stuff
 */

#define DEFAULT_SERVER_NAME "127.0.0.1"
#define DEFAULT_SERVER_PORT 53
#define DEFAULT_SERVER_TLS_PORT 853
#define DEFAULT_SERVER_PORTS "udp/tcp 53 or dot/tls 853"
#define DEFAULT_LOCAL_PORT 0
#define DEFAULT_SOCKET_BUFFER 32
#define DEFAULT_TIMEOUT 45
#define DEFAULT_MAX_OUTSTANDING (64 * 1024)

#define MAX_INPUT_DATA (64 * 1024)

#define TIMEOUT_CHECK_TIME 5000000

#define DNS_RCODE_NOERROR 0
#define DNS_RCODE_NXDOMAIN 3

struct query_info;

typedef perf_list(struct query_info) query_list;

typedef struct query_info {
    uint64_t sent_timestamp;
    /*
     * This link links the query into the list of outstanding
     * queries or the list of available query IDs.
     */
    perf_link(struct query_info);
    /*
     * The list this query is on.
     */
    query_list* list;
} query_info;

static query_list outstanding_list;
static query_list instanding_list;

static query_info* queries;

static perf_sockaddr_t         server_addr;
static perf_sockaddr_t         local_addr;
static unsigned int            nsocks;
static struct perf_net_socket* socks;
static enum perf_net_mode      mode;

static int dummypipe[2];

static uint64_t query_timeout;
static bool     edns;
static bool     dnssec;

static perf_datafile_t* input;

/* The target traffic level at the end of the ramp-up */
double max_qps = 100000.0;

/* The time period over which we ramp up traffic */
#define DEFAULT_RAMP_TIME 60
static uint64_t ramp_time;

/* How long to send constant traffic after the initial ramp-up */
#define DEFAULT_SUSTAIN_TIME 0
static uint64_t sustain_time;

/* How long to wait for responses after sending traffic */
static uint64_t wait_time = 40 * MILLION;

/* Total duration of the traffic-sending part of the test */
static uint64_t traffic_time;

/* Total duration of the test */
static uint64_t end_time;

/* Interval between plot data points, in microseconds */
#define DEFAULT_BUCKET_INTERVAL 0.5
static uint64_t bucket_interval;

/* The number of plot data points */
static int n_buckets;

/* The plot data file */
static const char* plotfile = "resperf.gnuplot";

/* The largest acceptable query loss when reporting max throughput */
static double max_loss_percent = 100.0;

/* The maximum number of outstanding queries */
static unsigned int max_outstanding;

static uint64_t num_queries_sent;
static uint64_t num_queries_outstanding;
static uint64_t num_responses_received;
static uint64_t num_queries_timed_out;
static uint64_t rcodecounts[16];

static uint64_t time_now;
static uint64_t time_of_program_start;
static uint64_t time_of_end_of_run;

/*
 * The last plot data point containing actual data; this can
 * be less than than (n_buckets - 1) if the traffic sending
 * phase is cut short
 */
static int last_bucket_used;

/*
 * The statistics for queries sent during one bucket_interval
 * of the traffic sending phase.
 */
typedef struct {
    int    queries;
    int    responses;
    int    failures;
    double latency_sum;
} ramp_bucket;

/* Pointer to array of n_buckets ramp_bucket structures */
static ramp_bucket* buckets;

enum phase {
    /*
     * The ramp-up phase: we are steadily increasing traffic.
     */
    PHASE_RAMP,
    /*
     * The sustain phase: we are sending traffic at a constant
     * rate.
     */
    PHASE_SUSTAIN,
    /*
     * The wait phase: we have stopped sending queries and are
     * just waiting for any remaining responses.
     */
    PHASE_WAIT
};
static enum phase phase = PHASE_RAMP;

/* The time when the sustain/wait phase began */
static uint64_t sustain_phase_began, wait_phase_began;

static perf_tsigkey_t* tsigkey;

static bool verbose;

const char* progname = "resperf";

static char*
stringify(double value, int precision)
{
    static char buf[20];

    snprintf(buf, sizeof(buf), "%.*f", precision, value);
    return buf;
}

static void
setup(int argc, char** argv)
{
    const char*  family      = NULL;
    const char*  server_name = DEFAULT_SERVER_NAME;
    in_port_t    server_port = 0;
    const char*  local_name  = NULL;
    in_port_t    local_port  = DEFAULT_LOCAL_PORT;
    const char*  filename    = NULL;
    const char*  tsigkey_str = NULL;
    int          sock_family;
    unsigned int bufsize;
    unsigned int i;
    const char*  _mode = 0;

    sock_family     = AF_UNSPEC;
    server_port     = 0;
    local_port      = DEFAULT_LOCAL_PORT;
    bufsize         = DEFAULT_SOCKET_BUFFER;
    query_timeout   = DEFAULT_TIMEOUT * MILLION;
    ramp_time       = DEFAULT_RAMP_TIME * MILLION;
    sustain_time    = DEFAULT_SUSTAIN_TIME * MILLION;
    bucket_interval = DEFAULT_BUCKET_INTERVAL * MILLION;
    max_outstanding = DEFAULT_MAX_OUTSTANDING;
    nsocks          = 1;
    mode            = sock_udp;
    verbose         = false;

    perf_opt_add('f', perf_opt_string, "family",
        "address family of DNS transport, inet or inet6", "any",
        &family);
    perf_opt_add('M', perf_opt_string, "mode", "set transport mode: udp, tcp or dot/tls", "udp", &_mode);
    perf_opt_add('s', perf_opt_string, "server_addr",
        "the server to query", DEFAULT_SERVER_NAME, &server_name);
    perf_opt_add('p', perf_opt_port, "port",
        "the port on which to query the server",
        DEFAULT_SERVER_PORTS, &server_port);
    perf_opt_add('a', perf_opt_string, "local_addr",
        "the local address from which to send queries", NULL,
        &local_name);
    perf_opt_add('x', perf_opt_port, "local_port",
        "the local port from which to send queries",
        stringify(DEFAULT_LOCAL_PORT, 0), &local_port);
    perf_opt_add('d', perf_opt_string, "datafile",
        "the input data file", "stdin", &filename);
    perf_opt_add('t', perf_opt_timeval, "timeout",
        "the timeout for query completion in seconds",
        stringify(DEFAULT_TIMEOUT, 0), &query_timeout);
    perf_opt_add('b', perf_opt_uint, "buffer_size",
        "socket send/receive buffer size in kilobytes", NULL,
        &bufsize);
    perf_opt_add('e', perf_opt_boolean, NULL,
        "enable EDNS 0", NULL, &edns);
    perf_opt_add('D', perf_opt_boolean, NULL,
        "set the DNSSEC OK bit (implies EDNS)", NULL, &dnssec);
    perf_opt_add('y', perf_opt_string, "[alg:]name:secret",
        "the TSIG algorithm, name and secret", NULL, &tsigkey_str);
    perf_opt_add('i', perf_opt_timeval, "plot_interval",
        "the time interval between plot data points, in seconds",
        stringify(DEFAULT_BUCKET_INTERVAL, 1), &bucket_interval);
    perf_opt_add('m', perf_opt_double, "max_qps",
        "the maximum number of queries per second",
        stringify(max_qps, 0), &max_qps);
    perf_opt_add('P', perf_opt_string, "plotfile",
        "the name of the plot data file", plotfile, &plotfile);
    perf_opt_add('r', perf_opt_timeval, "ramp_time",
        "the ramp-up time in seconds",
        stringify(DEFAULT_RAMP_TIME, 0), &ramp_time);
    perf_opt_add('c', perf_opt_timeval, "constant_traffic_time",
        "how long to send constant traffic, in seconds",
        stringify(DEFAULT_SUSTAIN_TIME, 0), &sustain_time);
    perf_opt_add('L', perf_opt_double, "max_query_loss",
        "the maximum acceptable query loss, in percent",
        stringify(max_loss_percent, 0), &max_loss_percent);
    perf_opt_add('C', perf_opt_uint, "clients",
        "the number of clients to act as", NULL, &nsocks);
    perf_opt_add('q', perf_opt_uint, "num_outstanding",
        "the maximum number of queries outstanding",
        stringify(DEFAULT_MAX_OUTSTANDING, 0), &max_outstanding);
    perf_opt_add('v', perf_opt_boolean, NULL,
        "verbose: report additional information to stdout",
        NULL, &verbose);
    bool log_stdout = false;
    perf_opt_add('W', perf_opt_boolean, NULL, "log warnings and errors to stdout instead of stderr", NULL, &log_stdout);

    perf_opt_parse(argc, argv);

    if (log_stdout) {
        perf_log_tostdout();
    }

    if (_mode != 0)
        mode = perf_net_parsemode(_mode);

    if (!server_port) {
        server_port = mode == sock_tls ? DEFAULT_SERVER_TLS_PORT : DEFAULT_SERVER_PORT;
    }

    if (max_outstanding > nsocks * DEFAULT_MAX_OUTSTANDING)
        perf_log_fatal("number of outstanding packets (%u) must not "
                       "be more than 64K per client",
            max_outstanding);

    if (ramp_time + sustain_time == 0)
        perf_log_fatal("rampup_time and constant_traffic_time must not "
                       "both be 0");

    perf_list_init(outstanding_list);
    perf_list_init(instanding_list);
    if (!(queries = calloc(max_outstanding, sizeof(query_info)))) {
        perf_log_fatal("out of memory");
    }
    for (i = 0; i < max_outstanding; i++) {
        perf_link_init(&queries[i]);
        perf_list_append(instanding_list, &queries[i]);
        queries[i].list = &instanding_list;
    }

    if (family != NULL)
        sock_family = perf_net_parsefamily(family);
    perf_net_parseserver(sock_family, server_name, server_port, &server_addr);
    perf_net_parselocal(server_addr.sa.sa.sa_family, local_name,
        local_port, &local_addr);

    input = perf_datafile_open(filename);

    if (dnssec)
        edns = true;

    if (tsigkey_str != NULL)
        tsigkey = perf_tsig_parsekey(tsigkey_str);

    if (!(socks = calloc(nsocks, sizeof(*socks)))) {
        perf_log_fatal("out of memory");
    }
    for (i = 0; i < nsocks; i++)
        socks[i] = perf_net_opensocket(mode, &server_addr, &local_addr, i, bufsize);
}

static void
cleanup(void)
{
    unsigned int i;

    perf_datafile_close(&input);
    for (i = 0; i < nsocks; i++)
        (void)perf_net_close(&socks[i]);
    close(dummypipe[0]);
    close(dummypipe[1]);
}

/* Find the ramp_bucket for queries sent at time "when" */

static ramp_bucket*
find_bucket(uint64_t when)
{
    uint64_t sent_at = when - time_of_program_start;
    int      i       = (int)((n_buckets * sent_at) / traffic_time);
    /*
     * Guard against array bounds violations due to roundoff
     * errors or scheduling jitter
     */
    if (i < 0)
        i = 0;
    if (i > n_buckets - 1)
        i = n_buckets - 1;
    return &buckets[i];
}

/*
 * print_statistics:
 *   Print out statistics based on the results of the test
 */
static void
print_statistics(void)
{
    int      i;
    double   max_throughput;
    double   loss_at_max_throughput;
    bool     first_rcode;
    uint64_t run_time = time_of_end_of_run - time_of_program_start;

    printf("\nStatistics:\n\n");

    printf("  Queries sent:         %" PRIu64 "\n",
        num_queries_sent);
    printf("  Queries completed:    %" PRIu64 "\n",
        num_responses_received);
    printf("  Queries lost:         %" PRIu64 "\n",
        num_queries_sent - num_responses_received);
    printf("  Response codes:       ");
    first_rcode = true;
    for (i = 0; i < 16; i++) {
        if (rcodecounts[i] == 0)
            continue;
        if (first_rcode)
            first_rcode = false;
        else
            printf(", ");
        printf("%s %" PRIu64 " (%.2lf%%)",
            perf_dns_rcode_strings[i], rcodecounts[i],
            (rcodecounts[i] * 100.0) / num_responses_received);
    }
    printf("\n");
    printf("  Run time (s):         %u.%06u\n",
        (unsigned int)(run_time / MILLION),
        (unsigned int)(run_time % MILLION));

    /* Find the maximum throughput, subject to the -L option */
    max_throughput         = 0.0;
    loss_at_max_throughput = 0.0;
    for (i = 0; i <= last_bucket_used; i++) {
        ramp_bucket* b                 = &buckets[i];
        double       responses_per_sec = b->responses / (bucket_interval / (double)MILLION);
        double       loss              = b->queries ? (b->queries - b->responses) / (double)b->queries : 0.0;
        double       loss_percent      = loss * 100.0;
        if (loss_percent > max_loss_percent)
            break;
        if (responses_per_sec > max_throughput) {
            max_throughput         = responses_per_sec;
            loss_at_max_throughput = loss_percent;
        }
    }
    printf("  Maximum throughput:   %.6lf qps\n", max_throughput);
    printf("  Lost at that point:   %.2f%%\n", loss_at_max_throughput);
}

static ramp_bucket*
init_buckets(int n)
{
    ramp_bucket* p;
    int          i;

    if (!(p = calloc(n, sizeof(*p)))) {
        perf_log_fatal("out of memory");
        return 0; // fix clang scan-build
    }
    for (i = 0; i < n; i++) {
        p[i].queries = p[i].responses = p[i].failures = 0;
        p[i].latency_sum                              = 0.0;
    }
    return p;
}

/*
 * Send a query based on a line of input.
 * Return PERF_R_NOMORE if we ran out of query IDs.
 */
static perf_result_t
do_one_line(perf_buffer_t* lines, perf_buffer_t* msg)
{
    query_info*    q;
    unsigned int   qid;
    unsigned int   sock;
    perf_region_t  used;
    unsigned char* base;
    unsigned int   length;
    perf_result_t  result;

    q = perf_list_head(instanding_list);
    if (!q)
        return (PERF_R_NOMORE);
    qid  = (q - queries) / nsocks;
    sock = (q - queries) % nsocks;

    if (socks[sock].sending) {
        if (perf_net_sockready(&socks[sock], dummypipe[0], TIMEOUT_CHECK_TIME) == -1) {
            if (errno == EINPROGRESS) {
                if (verbose) {
                    perf_log_warning("network congested, packet sending in progress");
                }
            } else {
                if (verbose) {
                    char __s[256];
                    perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s)));
                }
            }
            return (PERF_R_FAILURE);
        }

        perf_list_unlink(instanding_list, q);
        perf_list_prepend(outstanding_list, q);
        q->list = &outstanding_list;

        num_queries_sent++;
        num_queries_outstanding++;

        q = perf_list_head(instanding_list);
        if (!q)
            return (PERF_R_NOMORE);
        qid  = (q - queries) / nsocks;
        sock = (q - queries) % nsocks;
    }

    switch (perf_net_sockready(&socks[sock], dummypipe[0], TIMEOUT_CHECK_TIME)) {
    case 0:
        if (verbose) {
            perf_log_warning("failed to send packet: socket %d not ready", sock);
        }
        return (PERF_R_FAILURE);
    case -1:
        perf_log_warning("failed to send packet: socket %d readiness check timed out", sock);
        return (PERF_R_FAILURE);
    default:
        break;
    }

    perf_buffer_clear(lines);
    result = perf_datafile_next(input, lines, false);
    if (result != PERF_R_SUCCESS)
        perf_log_fatal("ran out of query data");
    perf_buffer_usedregion(lines, &used);

    perf_buffer_clear(msg);
    result = perf_dns_buildrequest(&used, qid,
        edns, dnssec, false,
        tsigkey, 0,
        msg);
    if (result != PERF_R_SUCCESS)
        return (result);

    q->sent_timestamp = time_now;

    base   = perf_buffer_base(msg);
    length = perf_buffer_usedlength(msg);
    if (perf_net_sendto(&socks[sock], base, length, 0,
            &server_addr.sa.sa, server_addr.length)
        < 1) {
        if (errno == EINPROGRESS) {
            if (verbose) {
                perf_log_warning("network congested, packet sending in progress");
            }
        } else {
            if (verbose) {
                char __s[256];
                perf_log_warning("failed to send packet: %s", perf_strerror_r(errno, __s, sizeof(__s)));
            }
        }
        return (PERF_R_FAILURE);
    }

    perf_list_unlink(instanding_list, q);
    perf_list_prepend(outstanding_list, q);
    q->list = &outstanding_list;

    num_queries_sent++;
    num_queries_outstanding++;

    return PERF_R_SUCCESS;
}

static void
enter_sustain_phase(void)
{
    phase = PHASE_SUSTAIN;
    if (sustain_time != 0.0)
        printf("[Status] Ramp-up done, sending constant traffic\n");
    sustain_phase_began = time_now;
}

static void
enter_wait_phase(void)
{
    phase = PHASE_WAIT;
    printf("[Status] Waiting for more responses\n");
    wait_phase_began = time_now;
}

/*
 * try_process_response:
 *
 *   Receive from the given socket & process an individual response packet.
 *   Remove it from the list of open queries (status[]) and decrement the
 *   number of outstanding queries if it matches an open query.
 */
static void
try_process_response(unsigned int sockindex)
{
    unsigned char packet_buffer[MAX_EDNS_PACKET];
    uint16_t*     packet_header;
    uint16_t      qid, rcode;
    query_info*   q;
    double        latency;
    ramp_bucket*  b;
    int           n;

    packet_header = (uint16_t*)packet_buffer;
    n             = perf_net_recv(&socks[sockindex], packet_buffer, sizeof(packet_buffer), 0);
    if (n < 0) {
        if (errno == EAGAIN || errno == EINTR) {
            return;
        } else {
            char __s[256];
            perf_log_fatal("failed to receive packet: %s", perf_strerror_r(errno, __s, sizeof(__s)));
        }
    } else if (!n) {
        // Treat connection closed like try again until reconnection features are in
        return;
    } else if (n < 4) {
        perf_log_warning("received short response");
        return;
    }

    qid   = ntohs(packet_header[0]);
    rcode = ntohs(packet_header[1]) & 0xF;

    q = &queries[qid * nsocks + sockindex];
    if (q->list != &outstanding_list) {
        perf_log_warning("received a response with an unexpected id: %u", qid);
        return;
    }

    perf_list_unlink(outstanding_list, q);
    perf_list_append(instanding_list, q);
    q->list = &instanding_list;

    num_queries_outstanding--;

    latency = (time_now - q->sent_timestamp) / (double)MILLION;
    b       = find_bucket(q->sent_timestamp);
    b->responses++;
    if (!(rcode == DNS_RCODE_NOERROR || rcode == DNS_RCODE_NXDOMAIN))
        b->failures++;
    b->latency_sum += latency;
    num_responses_received++;
    rcodecounts[rcode]++;
}

static void
retire_old_queries(void)
{
    query_info* q;

    while (true) {
        q = perf_list_tail(outstanding_list);
        if (q == NULL || (time_now - q->sent_timestamp) < query_timeout)
            break;
        perf_list_unlink(outstanding_list, q);
        perf_list_append(instanding_list, q);
        q->list = &instanding_list;

        num_queries_outstanding--;
        num_queries_timed_out++;
    }
}

static inline int
num_scheduled(uint64_t time_since_start)
{
    if (phase == PHASE_RAMP) {
        return 0.5 * max_qps * (double)time_since_start * time_since_start / (ramp_time * MILLION);
    } else { /* PHASE_SUSTAIN */
        return 0.5 * max_qps * (ramp_time / (double)MILLION) + max_qps * (time_since_start - ramp_time) / (double)MILLION;
    }
}

static void
handle_sigpipe(int sig)
{
    (void)sig;
    switch (mode) {
    case sock_tcp:
    case sock_tls:
        // if connection is closed it will generate a signal
        perf_log_fatal("SIGPIPE received, connection(s) likely closed, can't continue");
        break;
    default:
        break;
    }
}

int main(int argc, char** argv)
{
    int           i;
    FILE*         plotf;
    perf_buffer_t lines, msg;
    char          input_data[MAX_INPUT_DATA];
    unsigned char outpacket_buffer[MAX_EDNS_PACKET];
    unsigned int  max_packet_size;
    unsigned int  current_sock;
    perf_result_t result;

    printf("DNS Resolution Performance Testing Tool\n"
           "Version " PACKAGE_VERSION "\n\n");

    (void)SSL_library_init();
#if OPENSSL_VERSION_NUMBER < 0x10100000L
    SSL_load_error_strings();
    OPENSSL_config(0);
#endif

    setup(argc, argv);

    if (pipe(dummypipe) < 0)
        perf_log_fatal("creating pipe");

    perf_os_handlesignal(SIGPIPE, handle_sigpipe);

    perf_buffer_init(&lines, input_data, sizeof(input_data));

    max_packet_size = edns ? MAX_EDNS_PACKET : MAX_UDP_PACKET;
    perf_buffer_init(&msg, outpacket_buffer, max_packet_size);

    traffic_time = ramp_time + sustain_time;
    end_time     = traffic_time + wait_time;

    n_buckets = (traffic_time + bucket_interval - 1) / bucket_interval;
    buckets   = init_buckets(n_buckets);

    time_now              = perf_get_time();
    time_of_program_start = time_now;

    printf("[Status] Command line: %s", progname);
    for (i = 1; i < argc; i++) {
        printf(" %s", argv[i]);
    }
    printf("\n");

    printf("[Status] Sending\n");

    current_sock = 0;
    for (;;) {
        int      should_send;
        uint64_t time_since_start = time_now - time_of_program_start;
        switch (phase) {
        case PHASE_RAMP:
            if (time_since_start >= ramp_time)
                enter_sustain_phase();
            break;
        case PHASE_SUSTAIN:
            if (time_since_start >= traffic_time)
                enter_wait_phase();
            break;
        case PHASE_WAIT:
            if (time_since_start >= end_time || perf_list_empty(outstanding_list))
                goto end_loop;
            break;
        }
        if (phase != PHASE_WAIT) {
            should_send = num_scheduled(time_since_start) - num_queries_sent;
            if (should_send >= 1000) {
                printf("[Status] Fell behind by %d queries, "
                       "ending test at %.0f qps\n",
                    should_send, (max_qps * time_since_start) / ramp_time);
                enter_wait_phase();
            }
            if (should_send > 0) {
                result = do_one_line(&lines, &msg);
                if (result == PERF_R_SUCCESS)
                    find_bucket(time_now)->queries++;
                if (result == PERF_R_NOMORE) {
                    printf("[Status] Reached %u outstanding queries\n",
                        max_outstanding);
                    enter_wait_phase();
                }
            }
        }
        try_process_response(current_sock++);
        current_sock = current_sock % nsocks;
        retire_old_queries();
        time_now = perf_get_time();
    }
end_loop:
    time_now           = perf_get_time();
    time_of_end_of_run = time_now;

    printf("[Status] Testing complete\n");

    plotf = fopen(plotfile, "w");
    if (!plotf) {
        char __s[256];
        perf_log_fatal("could not open %s: %s", plotfile, perf_strerror_r(errno, __s, sizeof(__s)));
    }

    /* Print column headers */
    fprintf(plotf, "# time target_qps actual_qps "
                   "responses_per_sec failures_per_sec avg_latency\n");

    /* Don't print unused buckets */
    last_bucket_used = find_bucket(wait_phase_began) - buckets;

    /* Don't print a partial bucket at the end */
    if (last_bucket_used > 0)
        --last_bucket_used;

    for (i = 0; i <= last_bucket_used; i++) {
        double t          = (i + 0.5) * traffic_time / (n_buckets * (double)MILLION);
        double ramp_dtime = ramp_time / (double)MILLION;
        double target_qps = t <= ramp_dtime ? (t / ramp_dtime) * max_qps : max_qps;
        double latency    = buckets[i].responses ? buckets[i].latency_sum / buckets[i].responses : 0;
        double interval   = bucket_interval / (double)MILLION;
        fprintf(plotf, "%7.3f %8.2f %8.2f %8.2f %8.2f %8.6f\n",
            t,
            target_qps,
            buckets[i].queries / interval,
            buckets[i].responses / interval,
            buckets[i].failures / interval,
            latency);
    }

    fclose(plotf);
    print_statistics();
    cleanup();
#if OPENSSL_VERSION_NUMBER < 0x10100000L
    ERR_free_strings();
#endif

    return 0;
}
