From 5482f8b5c95ea7e7073f6546750e39f470e455ef Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 13 Dec 2025 00:11:12 +0100 Subject: [PATCH] Initial commit. --- .gitea/workflows/build.yaml | 63 ++ Makefile | 38 ++ README.md | 96 +++ abr | Bin 0 -> 39928 bytes abr.py | 294 +++++++++ main.c | 1122 +++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 7 files changed, 1614 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 Makefile create mode 100644 README.md create mode 100755 abr create mode 100755 abr.py create mode 100644 main.c create mode 100644 requirements.txt 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 0000000000000000000000000000000000000000..5709c36577992e2e17a6646f2f3839ae09a40427 GIT binary patch literal 39928 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 S690KW&?)FW`^L)zO8*6;Bu2{s literal 0 HcmV?d00001 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