The visualization was six unrelated widgets - a histogram, a timeline, an error-rate bar, percentile bars, a fill gauge, plus a dashboard, heatmap and "real-time monitor" charting simulated CPU/memory/disk data. Colour was decorative and inconsistent, and the result did not read as a benchmark tool. Replace all of it with one drawing primitive: a vertical column chart with a zero-based labelled y-axis, a baseline and a labelled x-axis. Four charts share that shape - throughput over time (failures stacked in red), response time over time, response time distribution, and response time percentiles. Colour now carries meaning only: cyan is data, amber is the tail (p95+), red is failure. Charts size themselves to the terminal width. The summary keeps every number it had, laid out as aligned key/value blocks under section rules, with the ApacheBench percentile table as a 3-column grid. Fixes found along the way: - "Latency over the run" plotted the sorted durations, so it was always a monotonic ramp rather than a timeline. Requests now record a start offset relative to the run, so the time-axis charts show what actually happened. - The throughput gauge was scaled against a hardcoded 1000.0 req/s, so it read a near-zero percentage regardless of the target. - The live progress line recomputed statistics over every completed request on every completion, an O(n^2) cost inside the benchmark loop. It is now throttled to ~20 Hz and skipped entirely when stdout is not a terminal. Colour and the progress bar are gated on isatty(), so redirected output is plain, greppable text with no escape codes. A heavy tail (p50 57 ms, max 1525 ms) previously collapsed the distribution into a single column. The chart now clips its axis at p95 when the maximum dwarfs it and states so beneath the chart; well-behaved runs are never clipped and the full range stays in the footer either way. Also drop 77 lines of dead simulation code from main.c, wire graphs.c into the Makefile, untrack the built binary, and add a .gitignore. Builds warning-free at -Wall -Wextra; valgrind clean on the loaded, single-request and all-failed paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GCMSMcgcwnH9a8hhaLsg6j
1208 lines
43 KiB
C
1208 lines
43 KiB
C
// retoor <retoor@molodetz.nl>
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <time.h>
|
|
#include <math.h>
|
|
#include <unistd.h>
|
|
#include <locale.h>
|
|
#include <poll.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <arpa/inet.h>
|
|
#include <netdb.h>
|
|
#include <signal.h>
|
|
#include <openssl/ssl.h>
|
|
#include <openssl/err.h>
|
|
#include <stdbool.h>
|
|
#include <ctype.h>
|
|
#include <stdarg.h>
|
|
#include "graphs.h"
|
|
|
|
/* Styling is resolved at start-up so redirected output stays plain text. */
|
|
static const char *STYLE_RESET = "\033[0m";
|
|
static const char *STYLE_BOLD = "\033[1m";
|
|
static const char *STYLE_DIM = "\033[2m";
|
|
static const char *STYLE_RED = "\033[31m";
|
|
static const char *STYLE_GREEN = "\033[32m";
|
|
static const char *STYLE_YELLOW = "\033[33m";
|
|
static const char *STYLE_CYAN = "\033[36m";
|
|
|
|
static bool g_interactive = false;
|
|
|
|
static void init_output_style(void) {
|
|
g_interactive = isatty(STDOUT_FILENO) ? true : false;
|
|
graph_set_color(g_interactive);
|
|
if (!g_interactive) {
|
|
STYLE_RESET = STYLE_BOLD = STYLE_DIM = "";
|
|
STYLE_RED = STYLE_GREEN = STYLE_YELLOW = STYLE_CYAN = "";
|
|
}
|
|
}
|
|
|
|
#define MAX_HEADER_SIZE (16 * 1024)
|
|
#define INITIAL_BUFFER_SIZE (64 * 1024)
|
|
#define MAX_BUFFER_SIZE (16 * 1024 * 1024)
|
|
#define REQUEST_TIMEOUT_MS 30000
|
|
#define URL_SCHEME_MAX 16
|
|
#define URL_HOSTNAME_MAX 256
|
|
#define URL_PATH_MAX 2048
|
|
|
|
typedef struct {
|
|
char scheme[URL_SCHEME_MAX];
|
|
char hostname[URL_HOSTNAME_MAX];
|
|
int port;
|
|
char path[URL_PATH_MAX];
|
|
} url_t;
|
|
|
|
typedef struct {
|
|
long status;
|
|
double duration_ms;
|
|
double start_offset_ms; /* when the request was issued, from run start */
|
|
size_t body_size_bytes;
|
|
size_t header_size_bytes;
|
|
char server_software[128];
|
|
int failed;
|
|
char error[256];
|
|
} RequestResult;
|
|
|
|
typedef struct {
|
|
int sock;
|
|
SSL *ssl;
|
|
SSL_CTX *ctx;
|
|
bool is_https;
|
|
bool in_use;
|
|
bool chunked;
|
|
bool chunked_done;
|
|
size_t chunk_remaining;
|
|
int request_index;
|
|
char *send_buffer;
|
|
size_t send_offset;
|
|
size_t send_total;
|
|
char *recv_buffer;
|
|
size_t recv_offset;
|
|
size_t recv_capacity;
|
|
bool headers_complete;
|
|
long content_length;
|
|
struct timespec start_time;
|
|
} Connection;
|
|
|
|
static volatile sig_atomic_t g_shutdown_requested = 0;
|
|
static long total_connections_made = 0;
|
|
static Connection *connection_pool = NULL;
|
|
static int pool_size = 0;
|
|
static bool ssl_initialized = false;
|
|
|
|
static void signal_handler(int sig) {
|
|
(void)sig;
|
|
g_shutdown_requested = 1;
|
|
}
|
|
|
|
static void setup_signal_handlers(void) {
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_handler = signal_handler;
|
|
sigemptyset(&sa.sa_mask);
|
|
sa.sa_flags = 0;
|
|
sigaction(SIGINT, &sa, NULL);
|
|
sigaction(SIGTERM, &sa, NULL);
|
|
signal(SIGPIPE, SIG_IGN);
|
|
}
|
|
|
|
static int parse_url(const char *url_str, url_t *url) {
|
|
if (!url_str || !url) {
|
|
return -1;
|
|
}
|
|
memset(url, 0, sizeof(url_t));
|
|
|
|
const char *p = strstr(url_str, "://");
|
|
if (p) {
|
|
size_t scheme_len = (size_t)(p - url_str);
|
|
if (scheme_len >= URL_SCHEME_MAX) {
|
|
return -1;
|
|
}
|
|
memcpy(url->scheme, url_str, scheme_len);
|
|
url->scheme[scheme_len] = '\0';
|
|
url_str = p + 3;
|
|
} else {
|
|
strncpy(url->scheme, "http", URL_SCHEME_MAX - 1);
|
|
url->scheme[URL_SCHEME_MAX - 1] = '\0';
|
|
}
|
|
|
|
p = strchr(url_str, '/');
|
|
char host_port[URL_HOSTNAME_MAX + 8];
|
|
if (p) {
|
|
size_t hp_len = (size_t)(p - url_str);
|
|
if (hp_len >= sizeof(host_port)) {
|
|
return -1;
|
|
}
|
|
memcpy(host_port, url_str, hp_len);
|
|
host_port[hp_len] = '\0';
|
|
size_t path_len = strlen(p);
|
|
if (path_len >= URL_PATH_MAX) {
|
|
return -1;
|
|
}
|
|
strncpy(url->path, p, URL_PATH_MAX - 1);
|
|
url->path[URL_PATH_MAX - 1] = '\0';
|
|
} else {
|
|
size_t hp_len = strlen(url_str);
|
|
if (hp_len >= sizeof(host_port)) {
|
|
return -1;
|
|
}
|
|
strncpy(host_port, url_str, sizeof(host_port) - 1);
|
|
host_port[sizeof(host_port) - 1] = '\0';
|
|
strncpy(url->path, "/", URL_PATH_MAX - 1);
|
|
url->path[URL_PATH_MAX - 1] = '\0';
|
|
}
|
|
|
|
char *colon = strchr(host_port, ':');
|
|
if (colon) {
|
|
size_t host_len = (size_t)(colon - host_port);
|
|
if (host_len >= URL_HOSTNAME_MAX) {
|
|
return -1;
|
|
}
|
|
memcpy(url->hostname, host_port, host_len);
|
|
url->hostname[host_len] = '\0';
|
|
url->port = atoi(colon + 1);
|
|
if (url->port <= 0 || url->port > 65535) {
|
|
return -1;
|
|
}
|
|
} else {
|
|
if (strlen(host_port) >= URL_HOSTNAME_MAX) {
|
|
return -1;
|
|
}
|
|
strncpy(url->hostname, host_port, URL_HOSTNAME_MAX - 1);
|
|
url->hostname[URL_HOSTNAME_MAX - 1] = '\0';
|
|
url->port = (strcmp(url->scheme, "https") == 0) ? 443 : 80;
|
|
}
|
|
|
|
if (strlen(url->hostname) == 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void init_openssl(void) {
|
|
if (!ssl_initialized) {
|
|
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
|
OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL);
|
|
#else
|
|
SSL_library_init();
|
|
SSL_load_error_strings();
|
|
OpenSSL_add_all_algorithms();
|
|
#endif
|
|
ssl_initialized = true;
|
|
}
|
|
}
|
|
|
|
static void cleanup_openssl(void) {
|
|
if (ssl_initialized) {
|
|
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
|
EVP_cleanup();
|
|
ERR_free_strings();
|
|
#endif
|
|
ssl_initialized = false;
|
|
}
|
|
}
|
|
|
|
static SSL_CTX *create_context(bool verify_peer) {
|
|
const SSL_METHOD *method = TLS_client_method();
|
|
SSL_CTX *ctx = SSL_CTX_new(method);
|
|
if (!ctx) {
|
|
return NULL;
|
|
}
|
|
if (verify_peer) {
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
|
|
SSL_CTX_set_default_verify_paths(ctx);
|
|
} else {
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
static int create_socket(const char *hostname, int port) {
|
|
struct addrinfo hints, *result, *rp;
|
|
int sock = -1;
|
|
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_protocol = IPPROTO_TCP;
|
|
|
|
char port_str[8];
|
|
snprintf(port_str, sizeof(port_str), "%d", port);
|
|
|
|
int ret = getaddrinfo(hostname, port_str, &hints, &result);
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
|
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
|
if (sock < 0) {
|
|
continue;
|
|
}
|
|
|
|
int flags = fcntl(sock, F_GETFL, 0);
|
|
if (flags < 0) {
|
|
close(sock);
|
|
sock = -1;
|
|
continue;
|
|
}
|
|
if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
|
|
close(sock);
|
|
sock = -1;
|
|
continue;
|
|
}
|
|
|
|
ret = connect(sock, rp->ai_addr, rp->ai_addrlen);
|
|
if (ret < 0 && errno != EINPROGRESS) {
|
|
close(sock);
|
|
sock = -1;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
freeaddrinfo(result);
|
|
|
|
if (sock >= 0) {
|
|
total_connections_made++;
|
|
}
|
|
|
|
return sock;
|
|
}
|
|
|
|
static void release_connection(Connection *conn, bool keep_alive, int *active_connections);
|
|
|
|
static Connection* get_or_create_connection(url_t *url, int *active_connections, int max_connections, bool keep_alive, bool verify_ssl) {
|
|
if (keep_alive) {
|
|
for (int i = 0; i < pool_size; i++) {
|
|
if (!connection_pool[i].in_use && connection_pool[i].sock >= 0) {
|
|
connection_pool[i].in_use = true;
|
|
return &connection_pool[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (*active_connections >= max_connections) {
|
|
return NULL;
|
|
}
|
|
|
|
Connection *conn = NULL;
|
|
for (int i = 0; i < pool_size; i++) {
|
|
if (connection_pool[i].sock < 0) {
|
|
conn = &connection_pool[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!conn) {
|
|
int new_size = pool_size + 1;
|
|
Connection *new_pool = realloc(connection_pool, sizeof(Connection) * (size_t)new_size);
|
|
if (!new_pool) {
|
|
return NULL;
|
|
}
|
|
connection_pool = new_pool;
|
|
pool_size = new_size;
|
|
conn = &connection_pool[pool_size - 1];
|
|
}
|
|
|
|
memset(conn, 0, sizeof(Connection));
|
|
conn->sock = create_socket(url->hostname, url->port);
|
|
if (conn->sock < 0) {
|
|
conn->sock = -1;
|
|
return NULL;
|
|
}
|
|
|
|
conn->is_https = (strcmp(url->scheme, "https") == 0);
|
|
if (conn->is_https) {
|
|
init_openssl();
|
|
conn->ctx = create_context(verify_ssl);
|
|
if (!conn->ctx) {
|
|
close(conn->sock);
|
|
conn->sock = -1;
|
|
return NULL;
|
|
}
|
|
conn->ssl = SSL_new(conn->ctx);
|
|
if (!conn->ssl) {
|
|
SSL_CTX_free(conn->ctx);
|
|
conn->ctx = NULL;
|
|
close(conn->sock);
|
|
conn->sock = -1;
|
|
return NULL;
|
|
}
|
|
SSL_set_fd(conn->ssl, conn->sock);
|
|
SSL_set_connect_state(conn->ssl);
|
|
SSL_set_tlsext_host_name(conn->ssl, url->hostname);
|
|
}
|
|
|
|
conn->recv_buffer = malloc(INITIAL_BUFFER_SIZE);
|
|
if (!conn->recv_buffer) {
|
|
if (conn->ssl) {
|
|
SSL_free(conn->ssl);
|
|
conn->ssl = NULL;
|
|
}
|
|
if (conn->ctx) {
|
|
SSL_CTX_free(conn->ctx);
|
|
conn->ctx = NULL;
|
|
}
|
|
close(conn->sock);
|
|
conn->sock = -1;
|
|
return NULL;
|
|
}
|
|
conn->recv_capacity = INITIAL_BUFFER_SIZE;
|
|
conn->in_use = true;
|
|
conn->content_length = -1;
|
|
(*active_connections)++;
|
|
|
|
return conn;
|
|
}
|
|
|
|
static void release_connection(Connection *conn, bool keep_alive, int *active_connections) {
|
|
if (!conn) return;
|
|
|
|
if (keep_alive && conn->sock >= 0) {
|
|
conn->in_use = false;
|
|
conn->send_offset = 0;
|
|
conn->recv_offset = 0;
|
|
conn->headers_complete = false;
|
|
conn->content_length = -1;
|
|
conn->chunked = false;
|
|
conn->chunked_done = false;
|
|
conn->chunk_remaining = 0;
|
|
if (conn->send_buffer) {
|
|
free(conn->send_buffer);
|
|
conn->send_buffer = NULL;
|
|
}
|
|
} else {
|
|
if (conn->ssl) {
|
|
SSL_shutdown(conn->ssl);
|
|
SSL_free(conn->ssl);
|
|
conn->ssl = NULL;
|
|
}
|
|
if (conn->ctx) {
|
|
SSL_CTX_free(conn->ctx);
|
|
conn->ctx = NULL;
|
|
}
|
|
if (conn->sock >= 0) {
|
|
close(conn->sock);
|
|
conn->sock = -1;
|
|
}
|
|
if (conn->send_buffer) {
|
|
free(conn->send_buffer);
|
|
conn->send_buffer = NULL;
|
|
}
|
|
if (conn->recv_buffer) {
|
|
free(conn->recv_buffer);
|
|
conn->recv_buffer = NULL;
|
|
}
|
|
conn->recv_capacity = 0;
|
|
(*active_connections)--;
|
|
}
|
|
}
|
|
|
|
static long parse_status_code(const char *headers) {
|
|
const char *space = strchr(headers, ' ');
|
|
if (!space) return 0;
|
|
return atol(space + 1);
|
|
}
|
|
|
|
static char* get_header_value(const char *headers, const char *name) {
|
|
size_t name_len = strlen(name);
|
|
const char *p = headers;
|
|
|
|
while ((p = strchr(p, '\n')) != NULL) {
|
|
p++;
|
|
if (strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') {
|
|
const char *value_start = p + name_len + 1;
|
|
while (*value_start == ' ' || *value_start == '\t') {
|
|
value_start++;
|
|
}
|
|
const char *value_end = strstr(value_start, "\r\n");
|
|
if (!value_end) {
|
|
value_end = value_start + strlen(value_start);
|
|
}
|
|
size_t len = (size_t)(value_end - value_start);
|
|
char *value = malloc(len + 1);
|
|
if (!value) {
|
|
return NULL;
|
|
}
|
|
memcpy(value, value_start, len);
|
|
value[len] = '\0';
|
|
return value;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool is_chunked_encoding(const char *headers) {
|
|
char *te = get_header_value(headers, "Transfer-Encoding");
|
|
if (!te) {
|
|
return false;
|
|
}
|
|
bool chunked = (strcasestr(te, "chunked") != NULL);
|
|
free(te);
|
|
return chunked;
|
|
}
|
|
|
|
static size_t parse_chunk_size(const char *data, size_t *chunk_header_len) {
|
|
const char *end = strstr(data, "\r\n");
|
|
if (!end) {
|
|
*chunk_header_len = 0;
|
|
return 0;
|
|
}
|
|
*chunk_header_len = (size_t)(end - data) + 2;
|
|
char hex[20];
|
|
size_t hex_len = (size_t)(end - data);
|
|
if (hex_len >= sizeof(hex)) {
|
|
hex_len = sizeof(hex) - 1;
|
|
}
|
|
memcpy(hex, data, hex_len);
|
|
hex[hex_len] = '\0';
|
|
|
|
char *semicolon = strchr(hex, ';');
|
|
if (semicolon) {
|
|
*semicolon = '\0';
|
|
}
|
|
|
|
return (size_t)strtoul(hex, NULL, 16);
|
|
}
|
|
|
|
static const char* format_bytes(long long bytes) {
|
|
static char buffer[4][128];
|
|
static int idx = 0;
|
|
idx = (idx + 1) % 4;
|
|
|
|
const char *units[] = {"bytes", "KB", "MB", "GB", "TB"};
|
|
double value = (double)bytes;
|
|
int i = 0;
|
|
|
|
if (bytes < 1024) {
|
|
snprintf(buffer[idx], sizeof(buffer[idx]), "%lld bytes", bytes);
|
|
return buffer[idx];
|
|
}
|
|
|
|
while (value >= 1024.0 && i < 4) {
|
|
value /= 1024.0;
|
|
i++;
|
|
}
|
|
|
|
snprintf(buffer[idx], sizeof(buffer[idx]), "%.2f %s", value, units[i]);
|
|
return buffer[idx];
|
|
}
|
|
|
|
static int compare_doubles(const void *a, const void *b) {
|
|
double da = *(const double *)a;
|
|
double db = *(const double *)b;
|
|
if (da < db) return -1;
|
|
if (da > db) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static double get_mean(const double data[], int n) {
|
|
if (n <= 0) return 0.0;
|
|
double sum = 0.0;
|
|
for (int i = 0; i < n; ++i) sum += data[i];
|
|
return sum / n;
|
|
}
|
|
|
|
static double get_stdev(const double data[], int n) {
|
|
if (n < 2) return 0.0;
|
|
double mean = get_mean(data, n);
|
|
double sum_sq_diff = 0.0;
|
|
for (int i = 0; i < n; ++i) {
|
|
sum_sq_diff += (data[i] - mean) * (data[i] - mean);
|
|
}
|
|
return sqrt(sum_sq_diff / (n - 1));
|
|
}
|
|
|
|
/* Aligned key/value line; the shared shape of every summary block. */
|
|
static void kv(const char *key, const char *fmt, ...)
|
|
__attribute__((format(printf, 2, 3)));
|
|
|
|
static void kv(const char *key, const char *fmt, ...) {
|
|
va_list args;
|
|
printf(" %s%-18s%s", STYLE_DIM, key, STYLE_RESET);
|
|
va_start(args, fmt);
|
|
vprintf(fmt, args);
|
|
va_end(args);
|
|
putchar('\n');
|
|
}
|
|
|
|
/*
|
|
* One-line live status: a progress bar plus the running headline numbers.
|
|
* Only drawn on a terminal, and at most ~20 times a second, so the redraw
|
|
* never competes with the benchmark it is measuring.
|
|
*/
|
|
static void draw_progress(const RequestResult results[], int completed, int total,
|
|
const struct timespec *start, struct timespec *now) {
|
|
static double last_draw_ms = -1000.0;
|
|
const int bar_w = 24;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, now);
|
|
double elapsed_ms = (now->tv_sec - start->tv_sec) * 1000.0 +
|
|
(now->tv_nsec - start->tv_nsec) / 1e6;
|
|
|
|
if (!g_interactive) return;
|
|
if (completed < total && elapsed_ms - last_draw_ms < 50.0) return;
|
|
last_draw_ms = elapsed_ms;
|
|
|
|
long long bytes = 0;
|
|
double latency_sum = 0.0;
|
|
int ok = 0, failed = 0;
|
|
for (int i = 0; i < completed; ++i) {
|
|
bytes += (long long)(results[i].body_size_bytes + results[i].header_size_bytes);
|
|
if (results[i].failed) {
|
|
failed++;
|
|
} else {
|
|
ok++;
|
|
latency_sum += results[i].duration_ms;
|
|
}
|
|
}
|
|
|
|
double seconds = elapsed_ms / 1000.0;
|
|
double rps = seconds > 0 ? completed / seconds : 0.0;
|
|
double avg_latency_ms = ok > 0 ? latency_sum / ok : 0.0;
|
|
double kbytes_s = seconds > 0 ? (bytes / 1024.0) / seconds : 0.0;
|
|
int done = total > 0 ? completed * bar_w / total : 0;
|
|
|
|
fprintf(stdout, "\r\033[K %s", STYLE_CYAN);
|
|
for (int i = 0; i < done; ++i) fputs("â–ˆ", stdout);
|
|
fprintf(stdout, "%s%s", STYLE_RESET, STYLE_DIM);
|
|
for (int i = done; i < bar_w; ++i) fputs("â–‘", stdout);
|
|
int digits = snprintf(NULL, 0, "%d", total);
|
|
fprintf(stdout, "%s %3d%% %*d/%d %6.0f req/s %5.0f ms %7.0f KB/s %s%d failed%s",
|
|
STYLE_RESET, total > 0 ? completed * 100 / total : 0, digits, completed, total,
|
|
rps, avg_latency_ms, kbytes_s,
|
|
failed ? STYLE_RED : STYLE_DIM, failed, STYLE_RESET);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void print_summary(RequestResult results[], int total_requests, double total_duration_s, const char *url, int concurrency, long total_connections) {
|
|
double *request_durations_ms = malloc(sizeof(double) * (size_t)total_requests);
|
|
if (!request_durations_ms) {
|
|
fprintf(stderr, "Failed to allocate memory for statistics\n");
|
|
return;
|
|
}
|
|
|
|
int success_count = 0;
|
|
long long total_html_transferred = 0;
|
|
long long total_transferred = 0;
|
|
|
|
for (int i = 0; i < total_requests; ++i) {
|
|
if (!results[i].failed) {
|
|
request_durations_ms[success_count++] = results[i].duration_ms;
|
|
total_html_transferred += (long long)results[i].body_size_bytes;
|
|
total_transferred += (long long)(results[i].body_size_bytes + results[i].header_size_bytes);
|
|
}
|
|
}
|
|
|
|
int failed_count = total_requests - success_count;
|
|
|
|
if (success_count == 0) {
|
|
graph_section("Result");
|
|
kv("duration", "%.3f s", total_duration_s);
|
|
kv("requests", "%d", total_requests);
|
|
kv("failed", "%s%d (100%%)%s", STYLE_RED, failed_count, STYLE_RESET);
|
|
if (total_requests > 0 && results[0].error[0] != '\0') {
|
|
kv("error", "%s", results[0].error);
|
|
}
|
|
printf("\n");
|
|
free(request_durations_ms);
|
|
return;
|
|
}
|
|
|
|
url_t parsed_url;
|
|
if (parse_url(url, &parsed_url) != 0) {
|
|
strncpy(parsed_url.hostname, "unknown", URL_HOSTNAME_MAX - 1);
|
|
parsed_url.port = 0;
|
|
strncpy(parsed_url.path, "/", URL_PATH_MAX - 1);
|
|
}
|
|
|
|
RequestResult first_result = {0};
|
|
for (int i = 0; i < total_requests; ++i) {
|
|
if (!results[i].failed) {
|
|
first_result = results[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
double req_per_second = (double)total_requests / total_duration_s;
|
|
double time_per_req_concurrent = (total_duration_s * 1000) / total_requests;
|
|
double time_per_req_mean = (total_duration_s * 1000 * concurrency) / total_requests;
|
|
double transfer_rate_kbytes_s = (total_transferred / 1024.0) / total_duration_s;
|
|
|
|
qsort(request_durations_ms, (size_t)success_count, sizeof(double), compare_doubles);
|
|
|
|
double min_time = request_durations_ms[0];
|
|
double mean_time = get_mean(request_durations_ms, success_count);
|
|
double stdev_time = get_stdev(request_durations_ms, success_count);
|
|
double median_time = success_count % 2 ? request_durations_ms[success_count / 2] : (request_durations_ms[success_count / 2 - 1] + request_durations_ms[success_count / 2]) / 2.0;
|
|
double max_time = request_durations_ms[success_count - 1];
|
|
|
|
int percentile_points[] = {50, 66, 75, 80, 90, 95, 98, 99, 100};
|
|
double percentile_values[9];
|
|
for (int i = 0; i < 8; ++i) {
|
|
int index = (int)(success_count * percentile_points[i] / 100.0) - 1;
|
|
if (index < 0) index = 0;
|
|
percentile_values[i] = request_durations_ms[index];
|
|
}
|
|
percentile_values[8] = max_time;
|
|
|
|
const char *fail_color = (failed_count == 0) ? STYLE_GREEN : STYLE_RED;
|
|
char value[128];
|
|
|
|
graph_section("Target");
|
|
kv("server", "%s", first_result.server_software[0] ? first_result.server_software : "unknown");
|
|
kv("host", "%s:%d", parsed_url.hostname, parsed_url.port);
|
|
kv("path", "%s", parsed_url.path);
|
|
kv("document", "%s", format_bytes((long long)first_result.body_size_bytes));
|
|
|
|
graph_section("Result");
|
|
kv("concurrency", "%d", concurrency);
|
|
kv("duration", "%.3f s", total_duration_s);
|
|
kv("requests", "%d", total_requests);
|
|
kv("failed", "%s%d%s", fail_color, failed_count, STYLE_RESET);
|
|
kv("connections", "%ld", total_connections);
|
|
snprintf(value, sizeof value, "%s", format_bytes(total_transferred));
|
|
kv("transferred", "%s (%s html)", value, format_bytes(total_html_transferred));
|
|
kv("throughput", "%.2f req/s", req_per_second);
|
|
kv("transfer rate", "%.2f KB/s", transfer_rate_kbytes_s);
|
|
kv("time per request", "%.3f ms (%.3f ms across all concurrent)",
|
|
time_per_req_mean, time_per_req_concurrent);
|
|
|
|
graph_section("Connection times");
|
|
kv("min", "%.0f ms", min_time);
|
|
kv("mean", "%.0f ms", mean_time);
|
|
kv("sd", "%.1f ms", stdev_time);
|
|
kv("median", "%.0f ms", median_time);
|
|
kv("max", "%.0f ms", max_time);
|
|
|
|
graph_section("Percentiles");
|
|
for (int row = 0; row < 3; ++row) {
|
|
printf(" ");
|
|
for (int col = 0; col < 3; ++col) {
|
|
int i = col * 3 + row;
|
|
printf(" %s%3d%%%s %7.0f ms", STYLE_DIM, percentile_points[i], STYLE_RESET,
|
|
percentile_values[i]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
/* Charts: rate over time, latency over time, then the shape of the latency. */
|
|
double *finish_ms = malloc(sizeof(double) * (size_t)total_requests);
|
|
double *all_durations = malloc(sizeof(double) * (size_t)total_requests);
|
|
int *failed_flags = malloc(sizeof(int) * (size_t)total_requests);
|
|
|
|
if (finish_ms && all_durations && failed_flags) {
|
|
for (int i = 0; i < total_requests; ++i) {
|
|
finish_ms[i] = results[i].start_offset_ms + results[i].duration_ms;
|
|
all_durations[i] = results[i].duration_ms;
|
|
failed_flags[i] = results[i].failed;
|
|
}
|
|
graph_throughput(finish_ms, failed_flags, total_requests, total_duration_s);
|
|
graph_latency_over_time(finish_ms, all_durations, failed_flags, total_requests,
|
|
total_duration_s);
|
|
}
|
|
free(finish_ms);
|
|
free(all_durations);
|
|
free(failed_flags);
|
|
|
|
graph_latency_histogram(request_durations_ms, success_count);
|
|
graph_percentiles(request_durations_ms, success_count);
|
|
printf("\n");
|
|
|
|
free(request_durations_ms);
|
|
}
|
|
|
|
static double get_elapsed_ms(struct timespec *start) {
|
|
struct timespec now;
|
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
return ((now.tv_sec - start->tv_sec) * 1000.0) + ((now.tv_nsec - start->tv_nsec) / 1000000.0);
|
|
}
|
|
|
|
static void print_usage(const char *prog) {
|
|
fprintf(stderr, "Usage: %s -n <requests> -c <concurrency> [-k] [-i] <url>\n", prog);
|
|
fprintf(stderr, " -n Total number of requests to perform\n");
|
|
fprintf(stderr, " -c Number of concurrent connections\n");
|
|
fprintf(stderr, " -k Use HTTP Keep-Alive\n");
|
|
fprintf(stderr, " -i Insecure mode (skip SSL certificate verification)\n");
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
setlocale(LC_ALL, "");
|
|
init_output_style();
|
|
setup_signal_handlers();
|
|
|
|
int total_requests = 0;
|
|
int concurrency = 0;
|
|
int keep_alive = 0;
|
|
int insecure = 0;
|
|
char *url = NULL;
|
|
|
|
int opt;
|
|
while ((opt = getopt(argc, argv, "n:c:ki")) != -1) {
|
|
switch (opt) {
|
|
case 'n': {
|
|
char *endptr;
|
|
long val = strtol(optarg, &endptr, 10);
|
|
if (*endptr != '\0' || val <= 0 || val > INT_MAX) {
|
|
fprintf(stderr, "Error: Invalid value for -n: %s\n", optarg);
|
|
return 1;
|
|
}
|
|
total_requests = (int)val;
|
|
break;
|
|
}
|
|
case 'c': {
|
|
char *endptr;
|
|
long val = strtol(optarg, &endptr, 10);
|
|
if (*endptr != '\0' || val <= 0 || val > 10000) {
|
|
fprintf(stderr, "Error: Invalid value for -c: %s (max 10000)\n", optarg);
|
|
return 1;
|
|
}
|
|
concurrency = (int)val;
|
|
break;
|
|
}
|
|
case 'k':
|
|
keep_alive = 1;
|
|
break;
|
|
case 'i':
|
|
insecure = 1;
|
|
break;
|
|
default:
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (optind < argc) {
|
|
url = argv[optind];
|
|
}
|
|
|
|
if (total_requests <= 0 || concurrency <= 0 || url == NULL) {
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
if (total_requests < concurrency) {
|
|
fprintf(stderr, "Error: Number of requests (-n) cannot be less than the concurrency level (-c).\n");
|
|
return 1;
|
|
}
|
|
|
|
url_t parsed_url;
|
|
if (parse_url(url, &parsed_url) != 0) {
|
|
fprintf(stderr, "Error: Failed to parse URL: %s\n", url);
|
|
return 1;
|
|
}
|
|
|
|
printf("%sabr%s %s%d requests, concurrency %d%s\n",
|
|
STYLE_BOLD, STYLE_RESET, STYLE_DIM, total_requests, concurrency, STYLE_RESET);
|
|
printf("%s%s%s\n", STYLE_DIM, url, STYLE_RESET);
|
|
if (insecure && strcmp(parsed_url.scheme, "https") == 0) {
|
|
printf("%scertificate verification disabled%s\n", STYLE_YELLOW, STYLE_RESET);
|
|
}
|
|
|
|
RequestResult *results = calloc((size_t)total_requests, sizeof(RequestResult));
|
|
if (!results) {
|
|
fprintf(stderr, "Error: Failed to allocate memory for results\n");
|
|
return 1;
|
|
}
|
|
|
|
int requests_initiated = 0;
|
|
int requests_completed = 0;
|
|
int active_connections = 0;
|
|
|
|
struct timespec benchmark_start_time, current_time;
|
|
clock_gettime(CLOCK_MONOTONIC, &benchmark_start_time);
|
|
|
|
connection_pool = calloc((size_t)concurrency, sizeof(Connection));
|
|
if (!connection_pool) {
|
|
fprintf(stderr, "Error: Failed to allocate connection pool\n");
|
|
free(results);
|
|
return 1;
|
|
}
|
|
pool_size = concurrency;
|
|
for (int i = 0; i < concurrency; i++) {
|
|
connection_pool[i].sock = -1;
|
|
}
|
|
|
|
struct pollfd *poll_fds = malloc(sizeof(struct pollfd) * (size_t)concurrency);
|
|
int *poll_conn_map = malloc(sizeof(int) * (size_t)concurrency);
|
|
if (!poll_fds || !poll_conn_map) {
|
|
fprintf(stderr, "Error: Failed to allocate poll structures\n");
|
|
free(poll_fds);
|
|
free(poll_conn_map);
|
|
free(connection_pool);
|
|
free(results);
|
|
return 1;
|
|
}
|
|
|
|
while (requests_completed < total_requests && !g_shutdown_requested) {
|
|
int nfds = 0;
|
|
|
|
while (requests_initiated < total_requests && active_connections < concurrency && !g_shutdown_requested) {
|
|
Connection *conn = get_or_create_connection(&parsed_url, &active_connections, concurrency, keep_alive, !insecure);
|
|
if (!conn) break;
|
|
|
|
conn->request_index = requests_initiated;
|
|
clock_gettime(CLOCK_MONOTONIC, &conn->start_time);
|
|
results[requests_initiated].start_offset_ms =
|
|
(conn->start_time.tv_sec - benchmark_start_time.tv_sec) * 1000.0 +
|
|
(conn->start_time.tv_nsec - benchmark_start_time.tv_nsec) / 1e6;
|
|
|
|
char request[4096];
|
|
int req_len = snprintf(request, sizeof(request),
|
|
"GET %s HTTP/1.1\r\n"
|
|
"Host: %s\r\n"
|
|
"User-Agent: abr/1.0\r\n"
|
|
"Accept: */*\r\n"
|
|
"Connection: %s\r\n"
|
|
"\r\n",
|
|
parsed_url.path, parsed_url.hostname,
|
|
keep_alive ? "keep-alive" : "close");
|
|
|
|
if (req_len < 0 || req_len >= (int)sizeof(request)) {
|
|
snprintf(results[requests_initiated].error, sizeof(results[requests_initiated].error), "Request too large");
|
|
results[requests_initiated].failed = 1;
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
requests_initiated++;
|
|
continue;
|
|
}
|
|
|
|
conn->send_buffer = malloc((size_t)req_len + 1);
|
|
if (!conn->send_buffer) {
|
|
snprintf(results[requests_initiated].error, sizeof(results[requests_initiated].error), "Memory allocation failed");
|
|
results[requests_initiated].failed = 1;
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
requests_initiated++;
|
|
continue;
|
|
}
|
|
memcpy(conn->send_buffer, request, (size_t)req_len + 1);
|
|
conn->send_total = (size_t)req_len;
|
|
conn->send_offset = 0;
|
|
|
|
requests_initiated++;
|
|
}
|
|
|
|
for (int i = 0; i < pool_size; i++) {
|
|
Connection *conn = &connection_pool[i];
|
|
if (conn->sock >= 0 && conn->in_use) {
|
|
double elapsed = get_elapsed_ms(&conn->start_time);
|
|
if (elapsed > REQUEST_TIMEOUT_MS) {
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Request timeout (%.0fms)", elapsed);
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = elapsed;
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
continue;
|
|
}
|
|
|
|
poll_fds[nfds].fd = conn->sock;
|
|
poll_fds[nfds].events = POLLERR | POLLHUP;
|
|
if (conn->send_offset < conn->send_total) {
|
|
poll_fds[nfds].events |= POLLOUT;
|
|
} else {
|
|
poll_fds[nfds].events |= POLLIN;
|
|
}
|
|
poll_fds[nfds].revents = 0;
|
|
poll_conn_map[nfds] = i;
|
|
nfds++;
|
|
}
|
|
}
|
|
|
|
if (nfds == 0) {
|
|
usleep(1000);
|
|
continue;
|
|
}
|
|
|
|
int ready = poll(poll_fds, (nfds_t)nfds, 10);
|
|
|
|
if (ready < 0) {
|
|
if (errno == EINTR) continue;
|
|
break;
|
|
}
|
|
if (ready == 0) continue;
|
|
|
|
for (int p = 0; p < nfds; p++) {
|
|
if (poll_fds[p].revents == 0) continue;
|
|
|
|
int i = poll_conn_map[p];
|
|
Connection *conn = &connection_pool[i];
|
|
if (conn->sock < 0 || !conn->in_use) continue;
|
|
|
|
if (poll_fds[p].revents & (POLLERR | POLLNVAL)) {
|
|
int err = 0;
|
|
socklen_t errlen = sizeof(err);
|
|
getsockopt(conn->sock, SOL_SOCKET, SO_ERROR, &err, &errlen);
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Connection error: %s", err ? strerror(err) : "Unknown error");
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
continue;
|
|
}
|
|
|
|
if ((poll_fds[p].revents & POLLOUT) && conn->send_offset < conn->send_total) {
|
|
ssize_t sent;
|
|
if (conn->is_https && conn->ssl) {
|
|
sent = SSL_write(conn->ssl, conn->send_buffer + conn->send_offset,
|
|
(int)(conn->send_total - conn->send_offset));
|
|
if (sent <= 0) {
|
|
int ssl_err = SSL_get_error(conn->ssl, (int)sent);
|
|
if (ssl_err != SSL_ERROR_WANT_READ && ssl_err != SSL_ERROR_WANT_WRITE) {
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "SSL write error");
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
}
|
|
continue;
|
|
}
|
|
} else {
|
|
sent = send(conn->sock, conn->send_buffer + conn->send_offset,
|
|
conn->send_total - conn->send_offset, MSG_NOSIGNAL);
|
|
if (sent <= 0) {
|
|
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Send error: %s", strerror(errno));
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (sent > 0) {
|
|
conn->send_offset += (size_t)sent;
|
|
}
|
|
}
|
|
|
|
if (poll_fds[p].revents & POLLIN) {
|
|
if (conn->recv_offset >= conn->recv_capacity - 1) {
|
|
if (conn->recv_capacity >= MAX_BUFFER_SIZE) {
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Response too large");
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
continue;
|
|
}
|
|
size_t new_capacity = conn->recv_capacity * 2;
|
|
if (new_capacity > MAX_BUFFER_SIZE) {
|
|
new_capacity = MAX_BUFFER_SIZE;
|
|
}
|
|
char *new_buffer = realloc(conn->recv_buffer, new_capacity);
|
|
if (!new_buffer) {
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Memory allocation failed");
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
continue;
|
|
}
|
|
conn->recv_buffer = new_buffer;
|
|
conn->recv_capacity = new_capacity;
|
|
}
|
|
|
|
ssize_t received;
|
|
size_t space = conn->recv_capacity - conn->recv_offset - 1;
|
|
|
|
if (conn->is_https && conn->ssl) {
|
|
received = SSL_read(conn->ssl, conn->recv_buffer + conn->recv_offset, (int)space);
|
|
if (received <= 0) {
|
|
int ssl_err = SSL_get_error(conn->ssl, (int)received);
|
|
if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE) {
|
|
continue;
|
|
}
|
|
if (ssl_err == SSL_ERROR_ZERO_RETURN || received == 0) {
|
|
goto connection_closed;
|
|
}
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "SSL read error");
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
continue;
|
|
}
|
|
} else {
|
|
received = recv(conn->sock, conn->recv_buffer + conn->recv_offset, space, 0);
|
|
if (received <= 0) {
|
|
if (received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
|
continue;
|
|
}
|
|
if (received == 0) {
|
|
goto connection_closed;
|
|
}
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Recv error: %s", strerror(errno));
|
|
results[conn->request_index].failed = 1;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
conn->recv_offset += (size_t)received;
|
|
conn->recv_buffer[conn->recv_offset] = '\0';
|
|
|
|
if (!conn->headers_complete) {
|
|
char *header_end = strstr(conn->recv_buffer, "\r\n\r\n");
|
|
if (header_end) {
|
|
conn->headers_complete = true;
|
|
size_t header_size = (size_t)(header_end - conn->recv_buffer) + 4;
|
|
|
|
results[conn->request_index].header_size_bytes = header_size;
|
|
results[conn->request_index].status = parse_status_code(conn->recv_buffer);
|
|
|
|
char *server = get_header_value(conn->recv_buffer, "Server");
|
|
if (server) {
|
|
strncpy(results[conn->request_index].server_software, server, sizeof(results[conn->request_index].server_software) - 1);
|
|
results[conn->request_index].server_software[sizeof(results[conn->request_index].server_software) - 1] = '\0';
|
|
free(server);
|
|
}
|
|
|
|
conn->chunked = is_chunked_encoding(conn->recv_buffer);
|
|
|
|
if (!conn->chunked) {
|
|
char *content_length_str = get_header_value(conn->recv_buffer, "Content-Length");
|
|
if (content_length_str) {
|
|
conn->content_length = atol(content_length_str);
|
|
free(content_length_str);
|
|
} else {
|
|
conn->content_length = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (conn->headers_complete) {
|
|
char *header_end = strstr(conn->recv_buffer, "\r\n\r\n");
|
|
size_t header_size = (size_t)(header_end - conn->recv_buffer) + 4;
|
|
size_t body_received = conn->recv_offset - header_size;
|
|
|
|
bool complete = false;
|
|
size_t body_size = 0;
|
|
|
|
if (conn->chunked) {
|
|
char *body_start = header_end + 4;
|
|
size_t body_data_len = conn->recv_offset - header_size;
|
|
size_t pos = 0;
|
|
size_t decoded_size = 0;
|
|
|
|
while (pos < body_data_len && !conn->chunked_done) {
|
|
if (conn->chunk_remaining > 0) {
|
|
size_t available = body_data_len - pos;
|
|
size_t to_consume = (available < conn->chunk_remaining) ? available : conn->chunk_remaining;
|
|
decoded_size += to_consume;
|
|
pos += to_consume;
|
|
conn->chunk_remaining -= to_consume;
|
|
|
|
if (conn->chunk_remaining == 0) {
|
|
if (pos + 2 <= body_data_len && body_start[pos] == '\r' && body_start[pos + 1] == '\n') {
|
|
pos += 2;
|
|
} else if (pos + 2 > body_data_len) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
size_t chunk_header_len;
|
|
size_t chunk_size = parse_chunk_size(body_start + pos, &chunk_header_len);
|
|
|
|
if (chunk_header_len == 0) {
|
|
break;
|
|
}
|
|
|
|
if (chunk_size == 0) {
|
|
conn->chunked_done = true;
|
|
complete = true;
|
|
body_size = decoded_size;
|
|
break;
|
|
}
|
|
|
|
pos += chunk_header_len;
|
|
conn->chunk_remaining = chunk_size;
|
|
}
|
|
}
|
|
|
|
if (!complete && conn->chunked_done) {
|
|
complete = true;
|
|
body_size = decoded_size;
|
|
}
|
|
} else if (conn->content_length >= 0) {
|
|
complete = (body_received >= (size_t)conn->content_length);
|
|
if (complete) {
|
|
body_size = (size_t)conn->content_length;
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
if (complete) {
|
|
results[conn->request_index].body_size_bytes = body_size;
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
results[conn->request_index].failed = (results[conn->request_index].status >= 400);
|
|
|
|
requests_completed++;
|
|
draw_progress(results, requests_completed, total_requests,
|
|
&benchmark_start_time, ¤t_time);
|
|
|
|
release_connection(conn, keep_alive, &active_connections);
|
|
}
|
|
}
|
|
continue;
|
|
|
|
connection_closed:
|
|
if (!conn->headers_complete) {
|
|
snprintf(results[conn->request_index].error, sizeof(results[conn->request_index].error), "Connection closed prematurely");
|
|
results[conn->request_index].failed = 1;
|
|
} else if (conn->content_length < 0 && !conn->chunked) {
|
|
char *header_end = strstr(conn->recv_buffer, "\r\n\r\n");
|
|
size_t header_size = (size_t)(header_end - conn->recv_buffer) + 4;
|
|
results[conn->request_index].body_size_bytes = conn->recv_offset - header_size;
|
|
results[conn->request_index].failed = (results[conn->request_index].status >= 400);
|
|
}
|
|
results[conn->request_index].duration_ms = get_elapsed_ms(&conn->start_time);
|
|
requests_completed++;
|
|
release_connection(conn, false, &active_connections);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (g_shutdown_requested) {
|
|
printf("\n%sShutdown requested, cleaning up...%s\n", STYLE_YELLOW, STYLE_RESET);
|
|
}
|
|
|
|
struct timespec benchmark_end_time;
|
|
clock_gettime(CLOCK_MONOTONIC, &benchmark_end_time);
|
|
double total_duration = (benchmark_end_time.tv_sec - benchmark_start_time.tv_sec) +
|
|
(benchmark_end_time.tv_nsec - benchmark_start_time.tv_nsec) / 1e9;
|
|
|
|
if (g_interactive) printf("\n");
|
|
|
|
print_summary(results, requests_completed, total_duration, url, concurrency, total_connections_made);
|
|
|
|
for (int i = 0; i < pool_size; i++) {
|
|
if (connection_pool[i].sock >= 0) {
|
|
release_connection(&connection_pool[i], false, &active_connections);
|
|
}
|
|
}
|
|
free(poll_fds);
|
|
free(poll_conn_map);
|
|
free(connection_pool);
|
|
connection_pool = NULL;
|
|
pool_size = 0;
|
|
free(results);
|
|
cleanup_openssl();
|
|
|
|
return g_shutdown_requested ? 130 : 0;
|
|
}
|