commit 5482f8b5c95ea7e7073f6546750e39f470e455ef Author: retoor Date: Sat Dec 13 00:11:12 2025 +0100 Initial commit. diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..efa4cf5 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,63 @@ +# retoor + +name: Build and Test + +on: + push: + branches: + - main + - master + pull_request: + branches: + - main + - master + +jobs: + build-c: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libssl-dev valgrind + + - name: Build + run: make + + - name: Build debug + run: make debug + + valgrind: + runs-on: ubuntu-latest + needs: build-c + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libssl-dev valgrind + + - name: Run valgrind memory tests + run: make valgrind + + build-python: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: make py-install + + - name: Test Python version + run: make py-test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..676a119 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +# retoor + +CC = gcc +CFLAGS = -Wall -Wextra -O2 +CFLAGS_DEBUG = -Wall -Wextra -g -O0 +LDFLAGS = -lssl -lcrypto -lm + +TARGET = abr +TEST_URL = https://example.com/ +PYTHON = python3 + +all: $(TARGET) + +$(TARGET): main.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +main.o: main.c + $(CC) $(CFLAGS) -c main.c + +debug: clean + $(CC) $(CFLAGS_DEBUG) -o $(TARGET) main.c $(LDFLAGS) + +valgrind: debug + valgrind --leak-check=full --show-leak-kinds=definite,indirect,possible --errors-for-leak-kinds=definite,indirect,possible --error-exitcode=1 ./$(TARGET) -n 5 -c 2 -i $(TEST_URL) + +clean: + rm -f $(TARGET) main.o + +py-install: + $(PYTHON) -m pip install -r requirements.txt + +py-run: + $(PYTHON) abr.py -n 5 -c 2 -i $(TEST_URL) + +py-test: + $(PYTHON) abr.py -n 10 -c 5 -i $(TEST_URL) + +.PHONY: all clean debug valgrind py-install py-run py-test diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8c7955 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ + + +# abr + +HTTP benchmark tool inspired by ApacheBench. Available in C and Python implementations. + +## C Version + +Uses non-blocking sockets with poll() multiplexing and OpenSSL for TLS. + +### Requirements + +- GCC +- OpenSSL development libraries (libssl-dev) +- POSIX-compliant system (Linux, BSD, macOS) + +### Build + +```sh +make # build optimized binary +make debug # build with debug symbols +make valgrind # run memory leak tests +make clean # remove build artifacts +``` + +### Usage + +```sh +./abr -n -c [-k] [-i] +``` + +## Python Version + +Uses asyncio with aiohttp for concurrent HTTP requests. + +### Requirements + +- Python 3.7+ +- aiohttp + +### Install + +```sh +make py-install +``` + +### Usage + +```sh +python3 abr.py -n -c [-k] [-i] +make py-run # quick test run +make py-test # test with more requests +``` + +## Options + +| Option | Description | +|--------|-------------| +| `-n` | Total number of requests | +| `-c` | Concurrent connections (max 10000) | +| `-k` | Enable HTTP Keep-Alive | +| `-i` | Skip SSL certificate verification | + +## Example + +```sh +./abr -n 1000 -c 50 -k https://example.com/ +python3 abr.py -n 1000 -c 50 -k https://example.com/ +``` + +## Output + +- Requests per second +- Transfer rate (KB/s) +- Response time percentiles (50th, 66th, 75th, 80th, 90th, 95th, 98th, 99th) +- Connection time statistics (min, mean, median, max, standard deviation) + +## Technical Details + +### C Version + +- Event-driven architecture using poll() +- Connection pooling with keep-alive support +- Chunked transfer-encoding support +- IPv4 and IPv6 via getaddrinfo() +- 30-second per-request timeout +- Graceful shutdown on SIGINT/SIGTERM +- OpenSSL 1.0.x and 1.1+ compatibility +- Memory leak free (verified with valgrind) + +### Python Version + +- Async I/O with asyncio and aiohttp +- Connection pooling with keep-alive support +- 30-second per-request timeout +- Graceful shutdown on SIGINT/SIGTERM diff --git a/abr b/abr new file mode 100755 index 0000000..5709c36 Binary files /dev/null and b/abr differ diff --git a/abr.py b/abr.py new file mode 100755 index 0000000..97b8618 --- /dev/null +++ b/abr.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +# retoor + +import asyncio +import aiohttp +import ssl +import time +import statistics +import argparse +import signal +import sys +from urllib.parse import urlparse +from typing import List, Dict, Any, Optional + +REQUEST_TIMEOUT_S = 30 +MAX_CONNECTIONS = 10000 + +class Style: + RESET = '\033[0m' + BOLD = '\033[1m' + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + CYAN = '\033[36m' + +shutdown_requested = False + +def signal_handler(sig, frame): + global shutdown_requested + shutdown_requested = True + +signal.signal(signal.SIGINT, signal_handler) +signal.signal(signal.SIGTERM, signal_handler) + +async def fetch(session: aiohttp.ClientSession, semaphore: asyncio.Semaphore, url: str, timeout: aiohttp.ClientTimeout) -> Dict[str, Any]: + async with semaphore: + start_time = time.monotonic() + try: + async with session.get(url, timeout=timeout) as response: + body_bytes = await response.read() + end_time = time.monotonic() + + header_size = sum(len(k) + len(v) + 4 for k, v in response.raw_headers) + 2 + + return { + "status": response.status, + "duration_ms": (end_time - start_time) * 1000, + "body_size_bytes": len(body_bytes), + "header_size_bytes": header_size, + "server_software": response.headers.get("Server"), + "failed": response.status >= 400, + "error": None + } + except asyncio.TimeoutError: + end_time = time.monotonic() + return { + "status": None, + "duration_ms": (end_time - start_time) * 1000, + "body_size_bytes": 0, + "header_size_bytes": 0, + "server_software": None, + "failed": True, + "error": f"Request timeout ({REQUEST_TIMEOUT_S}s)" + } + except aiohttp.ClientError as e: + end_time = time.monotonic() + return { + "status": None, + "duration_ms": (end_time - start_time) * 1000, + "body_size_bytes": 0, + "header_size_bytes": 0, + "server_software": None, + "failed": True, + "error": str(e) + } + except Exception as e: + end_time = time.monotonic() + return { + "status": None, + "duration_ms": (end_time - start_time) * 1000, + "body_size_bytes": 0, + "header_size_bytes": 0, + "server_software": None, + "failed": True, + "error": str(e) + } + +def format_bytes(bytes_val: int) -> str: + if bytes_val < 1024: + return f"{bytes_val} bytes" + units = ["KB", "MB", "GB", "TB"] + value = bytes_val / 1024 + for unit in units: + if value < 1024: + return f"{value:.2f} {unit}" + value /= 1024 + return f"{value:.2f} TB" + +def print_summary(results: List[Dict[str, Any]], total_duration_s: float, url: str, total_requests: int, concurrency: int, total_connections: int): + success_results = [r for r in results if not r["failed"]] + failed_count = len(results) - len(success_results) + + if not success_results: + print(f"{Style.RED}All requests failed. Cannot generate a detailed summary.{Style.RESET}") + print(f"Total time: {total_duration_s:.3f} seconds") + print(f"Failed requests: {failed_count}") + if results and results[0]['error']: + print(f"Sample error: {results[0]['error']}") + return + + parsed = urlparse(url) + hostname = parsed.hostname or "unknown" + port = parsed.port or (443 if parsed.scheme == "https" else 80) + path = parsed.path or "/" + if parsed.query: + path += "?" + parsed.query + + first_result = success_results[0] + doc_length = first_result["body_size_bytes"] + + request_durations_ms = [r["duration_ms"] for r in success_results] + total_html_transferred = sum(r["body_size_bytes"] for r in success_results) + total_transferred = sum(r["body_size_bytes"] + r["header_size_bytes"] for r in success_results) + + req_per_second = total_requests / total_duration_s + time_per_req_concurrent = (total_duration_s * 1000) / total_requests + time_per_req_mean = (total_duration_s * 1000 * concurrency) / total_requests + transfer_rate_kbytes_s = (total_transferred / 1024) / total_duration_s + + min_time = min(request_durations_ms) + mean_time = statistics.mean(request_durations_ms) + stdev_time = statistics.stdev(request_durations_ms) if len(request_durations_ms) > 1 else 0 + median_time = statistics.median(request_durations_ms) + max_time = max(request_durations_ms) + + sorted_durations = sorted(request_durations_ms) + n = len(sorted_durations) + percentiles = {} + for p in [50, 66, 75, 80, 90, 95, 98, 99]: + idx = max(0, int(n * p / 100) - 1) + percentiles[p] = sorted_durations[idx] + percentiles[100] = max_time + + y, g, r, c, b, rs = Style.YELLOW, Style.GREEN, Style.RED, Style.CYAN, Style.BOLD, Style.RESET + fail_color = g if failed_count == 0 else r + + print(f"{y}Server Software:{rs} {first_result['server_software'] or 'N/A'}") + print(f"{y}Server Hostname:{rs} {hostname}") + print(f"{y}Server Port:{rs} {port}\n") + print(f"{y}Document Path:{rs} {path}") + print(f"{y}Document Length:{rs} {format_bytes(doc_length)}\n") + print(f"{y}Concurrency Level:{rs} {concurrency}") + print(f"{y}Time taken for tests:{rs} {total_duration_s:.3f} seconds") + print(f"{y}Complete requests:{rs} {total_requests}") + print(f"{y}Failed requests:{rs} {fail_color}{failed_count}{rs}") + print(f"{y}Total connections made:{rs} {total_connections}") + print(f"{y}Total transferred:{rs} {format_bytes(total_transferred)}") + print(f"{y}HTML transferred:{rs} {format_bytes(total_html_transferred)}") + print(f"{y}Requests per second:{rs} {g}{req_per_second:.2f}{rs} [#/sec] (mean)") + print(f"{y}Time per request:{rs} {time_per_req_mean:.3f} [ms] (mean)") + print(f"{y}Time per request:{rs} {time_per_req_concurrent:.3f} [ms] (mean, across all concurrent requests)") + print(f"{y}Transfer rate:{rs} {g}{transfer_rate_kbytes_s:.2f}{rs} [Kbytes/sec] received\n") + + print(f"{c}{b}Connection Times (ms){rs}") + print(f"{c}---------------------{rs}") + print(f"{'min:':<10}{min_time:>8.0f}") + print(f"{'mean:':<10}{mean_time:>8.0f}") + print(f"{'sd:':<10}{stdev_time:>8.1f}") + print(f"{'median:':<10}{median_time:>8.0f}") + print(f"{'max:':<10}{max_time:>8.0f}\n") + + print(f"{c}{b}Percentage of the requests served within a certain time (ms){rs}") + for p, t in percentiles.items(): + print(f" {g}{p:>3}%{rs} {t:.0f}") + +async def main(url: str, total_requests: int, concurrency: int, keep_alive: bool, insecure: bool): + global shutdown_requested + + parsed = urlparse(url) + hostname = parsed.hostname or "unknown" + + print("abr, a Python-based HTTP benchmark inspired by ApacheBench.") + print(f"Benchmarking {hostname} (be patient)...") + + if insecure and parsed.scheme == "https": + print(f"{Style.YELLOW}Warning: SSL certificate verification disabled{Style.RESET}") + + semaphore = asyncio.Semaphore(concurrency) + timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT_S) + + ssl_context: Optional[ssl.SSLContext] = None + if insecure: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + connector = aiohttp.TCPConnector( + limit=min(concurrency, MAX_CONNECTIONS), + force_close=not keep_alive, + ssl=ssl_context if insecure else None + ) + + total_connections = 0 + + async with aiohttp.ClientSession(connector=connector) as session: + tasks = [asyncio.create_task(fetch(session, semaphore, url, timeout)) for _ in range(total_requests)] + + results = [] + completed_count = 0 + failed_count = 0 + success_count = 0 + total_duration_ms = 0 + total_bytes_transferred = 0 + + benchmark_start_time = time.monotonic() + + for future in asyncio.as_completed(tasks): + if shutdown_requested: + for task in tasks: + if not task.done(): + task.cancel() + break + + try: + result = await future + except asyncio.CancelledError: + break + + results.append(result) + + completed_count += 1 + total_connections += 1 + total_bytes_transferred += result["body_size_bytes"] + result["header_size_bytes"] + + if result["failed"]: + failed_count += 1 + else: + success_count += 1 + total_duration_ms += result["duration_ms"] + + elapsed_time = time.monotonic() - benchmark_start_time + req_per_sec = completed_count / elapsed_time if elapsed_time > 0 else 0 + avg_latency_ms = total_duration_ms / success_count if success_count > 0 else 0 + transfer_rate_kbs = (total_bytes_transferred / 1024) / elapsed_time if elapsed_time > 0 else 0 + + fail_color = Style.GREEN if failed_count == 0 else Style.RED + status_line = ( + f"\r{Style.BOLD}Completed: {completed_count}/{total_requests} | " + f"Failed: {fail_color}{failed_count}{Style.RESET}{Style.BOLD} | " + f"RPS: {Style.GREEN}{req_per_sec:.1f}{Style.RESET}{Style.BOLD} | " + f"Avg Latency: {avg_latency_ms:.0f}ms | " + f"Rate: {transfer_rate_kbs:.1f} KB/s{Style.RESET}" + ) + + sys.stdout.write(status_line) + sys.stdout.flush() + + benchmark_end_time = time.monotonic() + + if shutdown_requested: + print(f"\n{Style.YELLOW}Shutdown requested, cleaning up...{Style.RESET}") + + sys.stdout.write("\n\n") + print(f"{Style.GREEN}{Style.BOLD}Finished {len(results)} requests{Style.RESET}\n") + + total_duration = benchmark_end_time - benchmark_start_time + print_summary(results, total_duration, url, len(results), concurrency, total_connections) + + if shutdown_requested: + sys.exit(130) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="A Python-based HTTP benchmark tool inspired by ApacheBench.", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument('-n', type=int, required=True, help='Total number of requests to perform') + parser.add_argument('-c', type=int, required=True, help='Number of concurrent connections') + parser.add_argument('-k', action='store_true', help='Enable HTTP Keep-Alive') + parser.add_argument('-i', action='store_true', help='Insecure mode (skip SSL certificate verification)') + parser.add_argument('url', type=str, help='URL to benchmark') + + args = parser.parse_args() + + if args.n <= 0: + parser.error("Number of requests (-n) must be positive") + + if args.c <= 0 or args.c > MAX_CONNECTIONS: + parser.error(f"Concurrency (-c) must be between 1 and {MAX_CONNECTIONS}") + + if args.n < args.c: + parser.error("Number of requests (-n) cannot be less than the concurrency level (-c)") + + asyncio.run(main(url=args.url, total_requests=args.n, concurrency=args.c, keep_alive=args.k, insecure=args.i)) diff --git a/main.c b/main.c new file mode 100644 index 0000000..6e83153 --- /dev/null +++ b/main.c @@ -0,0 +1,1122 @@ +// retoor + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 -c [-k] [-i] \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, ¤t_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; +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6cea0c0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +aiohttp>=3.8.0