// 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>
#define STYLE_RESET "\033[0m"
#define STYLE_BOLD "\033[1m"
#define STYLE_RED "\033[31m"
#define STYLE_GREEN "\033[32m"
#define STYLE_YELLOW "\033[33m"
#define STYLE_CYAN "\033[36m"
#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;
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));
}
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) {
printf("%sAll requests failed. Cannot generate a detailed summary.%s\n", STYLE_RED, STYLE_RESET);
printf("Total time: %.3f seconds\n", total_duration_s);
printf("Failed requests: %d\n", failed_count);
if (total_requests > 0 && results[0].error[0] != '\0') {
printf("Sample error: %s\n", results[0].error);
}
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;
printf("%sServer Software:%s %s\n", STYLE_YELLOW, STYLE_RESET, strlen(first_result.server_software) > 0 ? first_result.server_software : "N/A");
printf("%sServer Hostname:%s %s\n", STYLE_YELLOW, STYLE_RESET, parsed_url.hostname);
printf("%sServer Port:%s %d\n\n", STYLE_YELLOW, STYLE_RESET, parsed_url.port);
printf("%sDocument Path:%s %s\n", STYLE_YELLOW, STYLE_RESET, parsed_url.path);
printf("%sDocument Length:%s %s\n\n", STYLE_YELLOW, STYLE_RESET, format_bytes((long long)first_result.body_size_bytes));
printf("%sConcurrency Level:%s %d\n", STYLE_YELLOW, STYLE_RESET, concurrency);
printf("%sTime taken for tests:%s %.3f seconds\n", STYLE_YELLOW, STYLE_RESET, total_duration_s);
printf("%sComplete requests:%s %d\n", STYLE_YELLOW, STYLE_RESET, total_requests);
printf("%sFailed requests:%s %s%d%s\n", STYLE_YELLOW, STYLE_RESET, fail_color, failed_count, STYLE_RESET);
printf("%sTotal connections made:%s %ld\n", STYLE_YELLOW, STYLE_RESET, total_connections);
printf("%sTotal transferred:%s %s\n", STYLE_YELLOW, STYLE_RESET, format_bytes(total_transferred));
printf("%sHTML transferred:%s %s\n", STYLE_YELLOW, STYLE_RESET, format_bytes(total_html_transferred));
printf("%sRequests per second:%s %s%.2f%s [#/sec] (mean)\n", STYLE_YELLOW, STYLE_RESET, STYLE_GREEN, req_per_second, STYLE_RESET);
printf("%sTime per request:%s %.3f [ms] (mean)\n", STYLE_YELLOW, STYLE_RESET, time_per_req_mean);
printf("%sTime per request:%s %.3f [ms] (mean, across all concurrent requests)\n", STYLE_YELLOW, STYLE_RESET, time_per_req_concurrent);
printf("%sTransfer rate:%s %s%.2f%s [Kbytes/sec] received\n\n", STYLE_YELLOW, STYLE_RESET, STYLE_GREEN, transfer_rate_kbytes_s, STYLE_RESET);
printf("%s%sConnection Times (ms)%s\n", STYLE_CYAN, STYLE_BOLD, STYLE_RESET);
printf("%s---------------------%s\n", STYLE_CYAN, STYLE_RESET);
printf("%-10s%8.0f\n", "min:", min_time);
printf("%-10s%8.0f\n", "mean:", mean_time);
printf("%-10s%8.1f\n", "sd:", stdev_time);
printf("%-10s%8.0f\n", "median:", median_time);
printf("%-10s%8.0f\n\n", "max:", max_time);
printf("%s%sPercentage of the requests served within a certain time (ms)%s\n", STYLE_CYAN, STYLE_BOLD, STYLE_RESET);
for (int i = 0; i < 9; ++i) {
printf(" %s%3d%%%s %.0f\n", STYLE_GREEN, percentile_points[i], STYLE_RESET, percentile_values[i]);
}
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, "");
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("abr, a C-based HTTP benchmark inspired by ApacheBench.\n");
printf("Benchmarking %s (be patient)...\n", parsed_url.hostname);
if (insecure && strcmp(parsed_url.scheme, "https") == 0) {
printf("%sWarning: SSL certificate 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);
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++;
long long total_bytes_transferred = 0;
double total_duration_ms = 0;
int success_count = 0;
int failed_count = 0;
for (int j = 0; j < requests_completed; ++j) {
total_bytes_transferred += (long long)(results[j].body_size_bytes + results[j].header_size_bytes);
if (results[j].failed) {
failed_count++;
} else {
success_count++;
total_duration_ms += results[j].duration_ms;
}
}
clock_gettime(CLOCK_MONOTONIC, &current_time);
double elapsed_time = (current_time.tv_sec - benchmark_start_time.tv_sec) +
(current_time.tv_nsec - benchmark_start_time.tv_nsec) / 1e9;
double req_per_sec = elapsed_time > 0 ? requests_completed / elapsed_time : 0;
double avg_latency_ms = success_count > 0 ? total_duration_ms / success_count : 0;
double transfer_rate_kbs = elapsed_time > 0 ? (total_bytes_transferred / 1024.0) / elapsed_time : 0;
const char *fail_color = (failed_count == 0) ? STYLE_GREEN : STYLE_RED;
fprintf(stdout, "\r%sCompleted: %d/%d | Failed: %s%d%s%s | RPS: %s%.1f%s%s | Avg Latency: %.0fms | Rate: %.1f KB/s%s",
STYLE_BOLD, requests_completed, total_requests,
fail_color, failed_count, STYLE_RESET, STYLE_BOLD,
STYLE_GREEN, req_per_sec, STYLE_RESET, STYLE_BOLD,
avg_latency_ms, transfer_rate_kbs, STYLE_RESET);
fflush(stdout);
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;
fprintf(stdout, "\n\n");
printf("%s%sFinished %d requests%s\n\n", STYLE_GREEN, STYLE_BOLD, requests_completed, STYLE_RESET);
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;
}