diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d86d0d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Build artifacts +abr +*.o + +# Tool logs +dpc.log + +# Python +__pycache__/ +*.py[cod] \ No newline at end of file diff --git a/Makefile b/Makefile index 676a119..db6f984 100644 --- a/Makefile +++ b/Makefile @@ -11,20 +11,23 @@ PYTHON = python3 all: $(TARGET) -$(TARGET): main.o +$(TARGET): main.o graphs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -main.o: main.c +graphs.o: graphs.c graphs.h + $(CC) $(CFLAGS) -c graphs.c + +main.o: main.c graphs.h $(CC) $(CFLAGS) -c main.c debug: clean - $(CC) $(CFLAGS_DEBUG) -o $(TARGET) main.c $(LDFLAGS) + $(CC) $(CFLAGS_DEBUG) -o $(TARGET) main.c graphs.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 + rm -f $(TARGET) main.o graphs.o py-install: $(PYTHON) -m pip install -r requirements.txt diff --git a/README.md b/README.md index a8c7955..e73d0bd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Uses non-blocking sockets with poll() multiplexing and OpenSSL for TLS. - GCC - OpenSSL development libraries (libssl-dev) - POSIX-compliant system (Linux, BSD, macOS) +- A UTF-8 capable terminal for the charts (block and box-drawing characters) ### Build @@ -29,6 +30,22 @@ make clean # remove build artifacts ./abr -n -c [-k] [-i] ``` +### Output + +abr prints a key/value summary (target, result, connection times, percentiles) +followed by four column charts, all drawn in the same style: a labelled y-axis +starting at zero, a baseline, and a labelled x-axis. + +- **Throughput over time** — completed requests per second per time slice; + failures stack on top of each column in red +- **Response time over time** — mean response time per time slice +- **Response time distribution** — how the response times are spread +- **Response time percentiles** — p50 through p100, tail (p95+) highlighted + +Charts size themselves to the terminal width. Colour and the live progress bar +are used only when stdout is a terminal, so `./abr ... > report.txt` produces +plain, greppable text with no escape codes. + ## Python Version Uses asyncio with aiohttp for concurrent HTTP requests. diff --git a/abr b/abr deleted file mode 100755 index 5709c36..0000000 Binary files a/abr and /dev/null differ diff --git a/graphs.c b/graphs.c new file mode 100644 index 0000000..cce7352 --- /dev/null +++ b/graphs.c @@ -0,0 +1,408 @@ +// retoor + +#include "graphs.h" + +#include +#include +#include +#include +#include +#include + +/* Layout ------------------------------------------------------------------ */ + +#define AXIS_W 7 /* five columns of y-label, a space, the axis glyph */ +#define LABEL_MAX 12 +#define MAX_BARS 48 +#define PLOT_H 10 /* labelled every other row, so gridlines land round */ + +/* Colour language: one accent for data, amber for the tail, red for failure. + Nothing else is coloured. */ +#define C_RESET "\033[0m" +#define C_BOLD "\033[1m" +#define C_DIM "\033[2m" +#define C_ACCENT "\033[36m" +#define C_TAIL "\033[33m" +#define C_FAIL "\033[31m" + +static int color_enabled = 1; + +void graph_set_color(int enabled) { color_enabled = enabled ? 1 : 0; } + +static const char *sgr(const char *code) { return color_enabled ? code : ""; } + +int graph_width(void) { + struct winsize ws; + int w = 78; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) { + w = ws.ws_col; + } + if (w > 96) w = 96; + if (w < 56) w = 56; + return w; +} + +/* How many bars fit while keeping each one at least min_cell columns wide. */ +static int bar_capacity(int min_cell) { + int n = (graph_width() - AXIS_W) / min_cell; + if (n > MAX_BARS) n = MAX_BARS; + if (n < 1) n = 1; + return n; +} + +/* Numbers ----------------------------------------------------------------- */ + +/* Round up to the next 1/2/2.5/5/10 x 10^n so the y-axis reads cleanly. */ +static double nice_ceil(double v) { + if (!(v > 0.0)) return 1.0; + double exp10 = pow(10.0, floor(log10(v))); + double m = v / exp10; + double nice = (m <= 1.0) ? 1.0 : (m <= 2.0) ? 2.0 : (m <= 2.5) ? 2.5 + : (m <= 5.0) ? 5.0 : 10.0; + return nice * exp10; +} + +/* Seconds on an x-axis: just enough precision for the length of the run. */ +static void fmt_seconds(char *buf, size_t size, double v, double total) { + if (total < 2.0) snprintf(buf, size, "%.2f", v); + else if (total < 20.0) snprintf(buf, size, "%.1f", v); + else snprintf(buf, size, "%.0f", v); +} + +static void fmt_num(char *buf, size_t size, double v) { + double a = fabs(v); + if (a >= 1000.0) snprintf(buf, size, "%.0f", v); + else if (a >= 100.0) snprintf(buf, size, "%.0f", v); + else if (a >= 10.0) snprintf(buf, size, "%.1f", v); + else if (a >= 1.0) snprintf(buf, size, "%.2f", v); + else snprintf(buf, size, "%.3f", v); +} + +/* Printed width of a string: skips ANSI escapes and UTF-8 continuation bytes. */ +static int visible_len(const char *str) { + int n = 0; + for (const unsigned char *p = (const unsigned char *)str; *p; p++) { + if (*p == 0x1b) { + while (*p && *p != 'm') p++; + if (!*p) break; + } else if ((*p & 0xC0) != 0x80) { + n++; + } + } + return n; +} + +/* Axis labels: only as many decimals as the gridline step actually needs. */ +static void fmt_axis(char *buf, size_t size, double v, double step) { + int decimals = 0; + if (step < 1.0) { + decimals = (step * 10.0 == floor(step * 10.0)) ? 1 : 2; + } else if (step < 10.0 && step != floor(step)) { + decimals = 1; + } + snprintf(buf, size, "%.*f", decimals, v); +} + +static int cmp_double(const void *a, const void *b) { + double x = *(const double *)a, y = *(const double *)b; + return (x < y) ? -1 : (x > y) ? 1 : 0; +} + +/* Renderer ---------------------------------------------------------------- */ + +void graph_section(const char *title) { + int width = graph_width(); + int len = (int)strlen(title); + printf("\n%s%s%s ", sgr(C_BOLD), title, sgr(C_RESET)); + printf("%s", sgr(C_DIM)); + for (int i = len + 1; i < width; i++) printf("─"); + printf("%s\n", sgr(C_RESET)); +} + +/* + * The single drawing primitive. `base` is the main series; `top` (optional) + * stacks on it and is drawn in red; `emph` (optional) flags columns to draw in + * the tail colour. Bars use half blocks so a column reads to half a row. + */ +static void render_bars(const char *title, const char *unit, + const double *base, const double *top, + const unsigned char *emph, + int count, const char labels[][LABEL_MAX], + const char *footer) { + if (count <= 0) return; + + int width = graph_width(); + int plot_w = width - AXIS_W; + int cell = plot_w / count; + if (cell < 1) cell = 1; + int bar_w = (cell > 1) ? cell - 1 : 1; + plot_w = cell * count; + + double ymax = 0.0; + for (int i = 0; i < count; i++) { + double v = base[i] + (top ? top[i] : 0.0); + if (v > ymax) ymax = v; + } + ymax = nice_ceil(ymax); + + /* Title left, unit right, on one line. */ + int tlen = visible_len(title); + int ulen = unit ? visible_len(unit) : 0; + int pad = width - tlen - ulen; + if (pad < 1) pad = 1; + printf("\n%s%s%s%*s%s%s%s\n", sgr(C_BOLD), title, sgr(C_RESET), + pad, "", sgr(C_DIM), unit ? unit : "", sgr(C_RESET)); + + + for (int row = PLOT_H - 1; row >= 0; row--) { + char ylab[LABEL_MAX] = ""; + if ((PLOT_H - 1 - row) % 2 == 0) { + fmt_axis(ylab, sizeof ylab, ymax * (row + 1) / PLOT_H, ymax * 2.0 / PLOT_H); + } + printf("%s%5s ┤%s", sgr(C_DIM), ylab, sgr(C_RESET)); + + /* Stop at the last column that has ink, so rows carry no trailing blanks. */ + int last = -1; + for (int i = 0; i < count; i++) { + if ((base[i] + (top ? top[i] : 0.0)) / ymax * PLOT_H - row >= 0.25) last = i; + } + + for (int i = 0; i <= last; i++) { + double filled = (base[i] + (top ? top[i] : 0.0)) / ymax * PLOT_H - row; + double solid = base[i] / ymax * PLOT_H - row; + const char *glyph = (filled >= 0.75) ? "█" + : (filled >= 0.25) ? "▄" : NULL; + if (glyph) { + const char *color = (solid > 0.0) + ? ((emph && emph[i]) ? sgr(C_TAIL) : sgr(C_ACCENT)) + : sgr(C_FAIL); + printf("%s", color); + for (int c = 0; c < bar_w; c++) printf("%s", glyph); + printf("%s", sgr(C_RESET)); + } else { + for (int c = 0; c < bar_w; c++) putchar(' '); + } + if (i < last) { + for (int c = bar_w; c < cell; c++) putchar(' '); + } + } + putchar('\n'); + } + + printf("%s%5s └", sgr(C_DIM), "0"); + for (int i = 0; i < plot_w; i++) printf("─"); + printf("%s\n", sgr(C_RESET)); + + /* X labels, centred under their bar and dropped where they would collide. */ + if (labels) { + char line[256]; + int n = (plot_w < (int)sizeof(line) - 1) ? plot_w : (int)sizeof(line) - 1; + memset(line, ' ', (size_t)n); + line[n] = '\0'; + int last_end = -1; + for (int i = 0; i < count; i++) { + int len = (int)strlen(labels[i]); + if (len == 0 || len > n) continue; + int start = i * cell + bar_w / 2 - len / 2; + if (start < 0) start = 0; + if (start + len > n) start = n - len; + if (start <= last_end) continue; + memcpy(line + start, labels[i], (size_t)len); + last_end = start + len; + } + while (n > 0 && line[n - 1] == ' ') line[--n] = '\0'; + printf("%s%*s%s%s\n", sgr(C_DIM), AXIS_W, "", line, sgr(C_RESET)); + } + + /* The footer may hold several lines; each is indented under the plot. */ + for (const char *p = footer; p && *p; ) { + const char *nl = strchr(p, '\n'); + int len = nl ? (int)(nl - p) : (int)strlen(p); + printf("%s%*s%.*s%s\n", sgr(C_DIM), AXIS_W, "", len, p, sgr(C_RESET)); + p = nl ? nl + 1 : p + len; + } +} + +/* Charts ------------------------------------------------------------------ */ + +void graph_latency_histogram(const double *durations_ms, int count) { + if (count <= 0) return; + + int buckets = bar_capacity(5); + if (buckets > 14) buckets = 14; + + double *sorted = malloc(sizeof(double) * (size_t)count); + if (!sorted) return; + memcpy(sorted, durations_ms, sizeof(double) * (size_t)count); + qsort(sorted, (size_t)count, sizeof(double), cmp_double); + + double lo = sorted[0]; + double hi = sorted[count - 1]; + double sum = 0.0; + for (int i = 0; i < count; i++) sum += durations_ms[i]; + double mean = sum / count; + double var = 0.0; + for (int i = 0; i < count; i++) var += (durations_ms[i] - mean) * (durations_ms[i] - mean); + double sd = (count > 1) ? sqrt(var / (count - 1)) : 0.0; + + /* A long tail would squash every column into the first bucket. When the + slowest request dwarfs p95, clip the axis there and say so in the + footer rather than quietly dropping the outliers. */ + int p95_idx = (int)(count * 0.95) - 1; + if (p95_idx < 0) p95_idx = 0; + double p95 = sorted[p95_idx]; + double axis_hi = hi; + int clipped = 0; + if (hi > p95 * 3.0 && p95 > lo) { + axis_hi = p95; + for (int i = count - 1; i >= 0 && sorted[i] > axis_hi; i--) clipped++; + } + free(sorted); + + /* A single distinct value has no distribution; show it as one column. */ + double span = axis_hi - lo; + if (span <= 0.0) { + span = 1.0; + buckets = 1; + } + + double values[MAX_BARS] = {0}; + char labels[MAX_BARS][LABEL_MAX]; + for (int i = 0; i < count; i++) { + if (durations_ms[i] > axis_hi) continue; + int b = (int)((durations_ms[i] - lo) / span * buckets); + if (b < 0) b = 0; + if (b >= buckets) b = buckets - 1; + values[b] += 1.0; + } + for (int b = 0; b < buckets; b++) { + fmt_num(labels[b], LABEL_MAX, lo + span * b / buckets); + } + + char footer[160]; + int written = snprintf(footer, sizeof footer, + "%d samples mean %.1f ms sd %.1f ms range %.0f-%.0f ms", + count, mean, sd, lo, hi); + if (clipped > 0 && written > 0 && written < (int)sizeof footer) { + snprintf(footer + written, sizeof footer - (size_t)written, + "\naxis clipped at p95 %.0f ms; %d slower requests are off the chart", + axis_hi, clipped); + } + + render_bars("Response time distribution", "y: requests x: ms", + values, NULL, NULL, buckets, labels, footer); +} + +void graph_throughput(const double *finish_ms, const int *failed, int count, + double total_seconds) { + if (count <= 0 || total_seconds <= 0.0) return; + + int buckets = bar_capacity(3); + if (buckets > 30) buckets = 30; + + double slice = total_seconds / buckets; + if (slice <= 0.0) return; + + double ok[MAX_BARS] = {0}, bad[MAX_BARS] = {0}; + char labels[MAX_BARS][LABEL_MAX]; + int failures = 0; + + for (int i = 0; i < count; i++) { + int b = (int)(finish_ms[i] / 1000.0 / slice); + if (b < 0) b = 0; + if (b >= buckets) b = buckets - 1; + if (failed[i]) { bad[b] += 1.0; failures++; } + else { ok[b] += 1.0; } + } + for (int b = 0; b < buckets; b++) { + ok[b] /= slice; + bad[b] /= slice; + fmt_seconds(labels[b], LABEL_MAX, b * slice, total_seconds); + } + + char footer[128]; + snprintf(footer, sizeof footer, + "%.1f req/s mean %d ok %d failed (%.2f%%)", + count / total_seconds, count - failures, failures, + (double)failures / count * 100.0); + + char unit[128]; + snprintf(unit, sizeof unit, "y: req/s x: seconds %s█%s ok %s█%s failed", + sgr(C_ACCENT), sgr(C_DIM), sgr(C_FAIL), sgr(C_DIM)); + + render_bars("Throughput over time", unit, ok, bad, NULL, buckets, labels, footer); +} + +void graph_latency_over_time(const double *finish_ms, const double *durations_ms, + const int *failed, int count, double total_seconds) { + if (count <= 0 || total_seconds <= 0.0) return; + + int buckets = bar_capacity(3); + if (buckets > 30) buckets = 30; + + double slice = total_seconds / buckets; + if (slice <= 0.0) return; + + double sum[MAX_BARS] = {0}, values[MAX_BARS] = {0}; + int n[MAX_BARS] = {0}; + char labels[MAX_BARS][LABEL_MAX]; + + for (int i = 0; i < count; i++) { + if (failed[i]) continue; + int b = (int)(finish_ms[i] / 1000.0 / slice); + if (b < 0) b = 0; + if (b >= buckets) b = buckets - 1; + sum[b] += durations_ms[i]; + n[b]++; + } + double peak = 0.0; + for (int b = 0; b < buckets; b++) { + values[b] = n[b] ? sum[b] / n[b] : 0.0; + if (values[b] > peak) peak = values[b]; + fmt_seconds(labels[b], LABEL_MAX, b * slice, total_seconds); + } + + char footer[128]; + snprintf(footer, sizeof footer, "mean per %.2f s slice peak %.0f ms", slice, peak); + + render_bars("Response time over time", "y: ms x: seconds", values, NULL, NULL, + buckets, labels, footer); +} + +void graph_percentiles(const double *durations_ms, int count) { + if (count <= 0) return; + + static const int points[] = {50, 66, 75, 80, 90, 95, 98, 99, 100}; + const int n_points = (int)(sizeof points / sizeof points[0]); + + double *sorted = malloc(sizeof(double) * (size_t)count); + if (!sorted) return; + memcpy(sorted, durations_ms, sizeof(double) * (size_t)count); + qsort(sorted, (size_t)count, sizeof(double), cmp_double); + + double values[MAX_BARS]; + unsigned char emph[MAX_BARS]; + char labels[MAX_BARS][LABEL_MAX]; + + for (int i = 0; i < n_points; i++) { + int idx = (int)(count * points[i] / 100.0) - 1; + if (idx < 0) idx = 0; + if (idx >= count) idx = count - 1; + values[i] = sorted[idx]; + emph[i] = (points[i] >= 95); + snprintf(labels[i], LABEL_MAX, "p%d", points[i]); + } + + char footer[128]; + snprintf(footer, sizeof footer, + "median %.0f ms p95 %.0f ms p99 %.0f ms max %.0f ms", + values[0], values[5], values[7], values[8]); + + char unit[128]; + snprintf(unit, sizeof unit, "y: ms %s█%s tail (p95+)", sgr(C_TAIL), sgr(C_DIM)); + + render_bars("Response time percentiles", unit, values, NULL, emph, n_points, + labels, footer); + + free(sorted); +} diff --git a/graphs.h b/graphs.h new file mode 100644 index 0000000..d91042b --- /dev/null +++ b/graphs.h @@ -0,0 +1,38 @@ +// retoor + +#ifndef GRAPHS_H +#define GRAPHS_H + +/* + * Column-chart renderer for abr. + * + * Every visual in abr is the same object: a vertical bar chart with a labelled + * y-axis, a baseline and an x-axis. One shape, one colour language, no + * decoration. Charts adapt to the terminal width and degrade to plain ASCII + * when colour is turned off. + */ + +/* Enable or disable ANSI colour. Call once at start-up (e.g. isatty(1)). */ +void graph_set_color(int enabled); + +/* Width in columns the charts will occupy, for aligning surrounding output. */ +int graph_width(void); + +/* Bold section rule, e.g. graph_section("Latency"). */ +void graph_section(const char *title); + +/* Distribution of response times. Input need not be sorted. */ +void graph_latency_histogram(const double *durations_ms, int count); + +/* Completed requests per time slice; failures stack on top in red. */ +void graph_throughput(const double *finish_ms, const int *failed, int count, + double total_seconds); + +/* Mean response time per time slice, over successful requests only. */ +void graph_latency_over_time(const double *finish_ms, const double *durations_ms, + const int *failed, int count, double total_seconds); + +/* Response time percentiles as columns; the tail (p95+) is highlighted. */ +void graph_percentiles(const double *durations_ms, int count); + +#endif diff --git a/main.c b/main.c index 6e83153..49243c7 100644 --- a/main.c +++ b/main.c @@ -20,13 +20,28 @@ #include #include #include +#include +#include "graphs.h" -#define STYLE_RESET "\033[0m" -#define STYLE_BOLD "\033[1m" -#define STYLE_RED "\033[31m" -#define STYLE_GREEN "\033[32m" -#define STYLE_YELLOW "\033[33m" -#define STYLE_CYAN "\033[36m" +/* Styling is resolved at start-up so redirected output stays plain text. */ +static const char *STYLE_RESET = "\033[0m"; +static const char *STYLE_BOLD = "\033[1m"; +static const char *STYLE_DIM = "\033[2m"; +static const char *STYLE_RED = "\033[31m"; +static const char *STYLE_GREEN = "\033[32m"; +static const char *STYLE_YELLOW = "\033[33m"; +static const char *STYLE_CYAN = "\033[36m"; + +static bool g_interactive = false; + +static void init_output_style(void) { + g_interactive = isatty(STDOUT_FILENO) ? true : false; + graph_set_color(g_interactive); + if (!g_interactive) { + STYLE_RESET = STYLE_BOLD = STYLE_DIM = ""; + STYLE_RED = STYLE_GREEN = STYLE_YELLOW = STYLE_CYAN = ""; + } +} #define MAX_HEADER_SIZE (16 * 1024) #define INITIAL_BUFFER_SIZE (64 * 1024) @@ -46,6 +61,7 @@ typedef struct { typedef struct { long status; double duration_ms; + double start_offset_ms; /* when the request was issued, from run start */ size_t body_size_bytes; size_t header_size_bytes; char server_software[128]; @@ -506,6 +522,68 @@ static double get_stdev(const double data[], int n) { return sqrt(sum_sq_diff / (n - 1)); } +/* Aligned key/value line; the shared shape of every summary block. */ +static void kv(const char *key, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +static void kv(const char *key, const char *fmt, ...) { + va_list args; + printf(" %s%-18s%s", STYLE_DIM, key, STYLE_RESET); + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + putchar('\n'); +} + +/* + * One-line live status: a progress bar plus the running headline numbers. + * Only drawn on a terminal, and at most ~20 times a second, so the redraw + * never competes with the benchmark it is measuring. + */ +static void draw_progress(const RequestResult results[], int completed, int total, + const struct timespec *start, struct timespec *now) { + static double last_draw_ms = -1000.0; + const int bar_w = 24; + + clock_gettime(CLOCK_MONOTONIC, now); + double elapsed_ms = (now->tv_sec - start->tv_sec) * 1000.0 + + (now->tv_nsec - start->tv_nsec) / 1e6; + + if (!g_interactive) return; + if (completed < total && elapsed_ms - last_draw_ms < 50.0) return; + last_draw_ms = elapsed_ms; + + long long bytes = 0; + double latency_sum = 0.0; + int ok = 0, failed = 0; + for (int i = 0; i < completed; ++i) { + bytes += (long long)(results[i].body_size_bytes + results[i].header_size_bytes); + if (results[i].failed) { + failed++; + } else { + ok++; + latency_sum += results[i].duration_ms; + } + } + + double seconds = elapsed_ms / 1000.0; + double rps = seconds > 0 ? completed / seconds : 0.0; + double avg_latency_ms = ok > 0 ? latency_sum / ok : 0.0; + double kbytes_s = seconds > 0 ? (bytes / 1024.0) / seconds : 0.0; + int done = total > 0 ? completed * bar_w / total : 0; + + fprintf(stdout, "\r\033[K %s", STYLE_CYAN); + for (int i = 0; i < done; ++i) fputs("█", stdout); + fprintf(stdout, "%s%s", STYLE_RESET, STYLE_DIM); + for (int i = done; i < bar_w; ++i) fputs("░", stdout); + int digits = snprintf(NULL, 0, "%d", total); + fprintf(stdout, "%s %3d%% %*d/%d %6.0f req/s %5.0f ms %7.0f KB/s %s%d failed%s", + STYLE_RESET, total > 0 ? completed * 100 / total : 0, digits, completed, total, + rps, avg_latency_ms, kbytes_s, + failed ? STYLE_RED : STYLE_DIM, failed, STYLE_RESET); + fflush(stdout); +} + static void print_summary(RequestResult results[], int total_requests, double total_duration_s, const char *url, int concurrency, long total_connections) { double *request_durations_ms = malloc(sizeof(double) * (size_t)total_requests); if (!request_durations_ms) { @@ -528,12 +606,14 @@ static void print_summary(RequestResult results[], int total_requests, double to 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); + graph_section("Result"); + kv("duration", "%.3f s", total_duration_s); + kv("requests", "%d", total_requests); + kv("failed", "%s%d (100%%)%s", STYLE_RED, failed_count, STYLE_RESET); if (total_requests > 0 && results[0].error[0] != '\0') { - printf("Sample error: %s\n", results[0].error); + kv("error", "%s", results[0].error); } + printf("\n"); free(request_durations_ms); return; } @@ -576,37 +656,68 @@ static void print_summary(RequestResult results[], int total_requests, double to percentile_values[8] = max_time; const char *fail_color = (failed_count == 0) ? STYLE_GREEN : STYLE_RED; + char value[128]; - 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); + graph_section("Target"); + kv("server", "%s", first_result.server_software[0] ? first_result.server_software : "unknown"); + kv("host", "%s:%d", parsed_url.hostname, parsed_url.port); + kv("path", "%s", parsed_url.path); + kv("document", "%s", format_bytes((long long)first_result.body_size_bytes)); - 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); + graph_section("Result"); + kv("concurrency", "%d", concurrency); + kv("duration", "%.3f s", total_duration_s); + kv("requests", "%d", total_requests); + kv("failed", "%s%d%s", fail_color, failed_count, STYLE_RESET); + kv("connections", "%ld", total_connections); + snprintf(value, sizeof value, "%s", format_bytes(total_transferred)); + kv("transferred", "%s (%s html)", value, format_bytes(total_html_transferred)); + kv("throughput", "%.2f req/s", req_per_second); + kv("transfer rate", "%.2f KB/s", transfer_rate_kbytes_s); + kv("time per request", "%.3f ms (%.3f ms across all concurrent)", + time_per_req_mean, time_per_req_concurrent); - 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]); + graph_section("Connection times"); + kv("min", "%.0f ms", min_time); + kv("mean", "%.0f ms", mean_time); + kv("sd", "%.1f ms", stdev_time); + kv("median", "%.0f ms", median_time); + kv("max", "%.0f ms", max_time); + + graph_section("Percentiles"); + for (int row = 0; row < 3; ++row) { + printf(" "); + for (int col = 0; col < 3; ++col) { + int i = col * 3 + row; + printf(" %s%3d%%%s %7.0f ms", STYLE_DIM, percentile_points[i], STYLE_RESET, + percentile_values[i]); + } + printf("\n"); } + /* Charts: rate over time, latency over time, then the shape of the latency. */ + double *finish_ms = malloc(sizeof(double) * (size_t)total_requests); + double *all_durations = malloc(sizeof(double) * (size_t)total_requests); + int *failed_flags = malloc(sizeof(int) * (size_t)total_requests); + + if (finish_ms && all_durations && failed_flags) { + for (int i = 0; i < total_requests; ++i) { + finish_ms[i] = results[i].start_offset_ms + results[i].duration_ms; + all_durations[i] = results[i].duration_ms; + failed_flags[i] = results[i].failed; + } + graph_throughput(finish_ms, failed_flags, total_requests, total_duration_s); + graph_latency_over_time(finish_ms, all_durations, failed_flags, total_requests, + total_duration_s); + } + free(finish_ms); + free(all_durations); + free(failed_flags); + + graph_latency_histogram(request_durations_ms, success_count); + graph_percentiles(request_durations_ms, success_count); + printf("\n"); + free(request_durations_ms); } @@ -626,6 +737,7 @@ static void print_usage(const char *prog) { int main(int argc, char *argv[]) { setlocale(LC_ALL, ""); + init_output_style(); setup_signal_handlers(); int total_requests = 0; @@ -689,10 +801,11 @@ int main(int argc, char *argv[]) { return 1; } - printf("abr, a C-based HTTP benchmark inspired by ApacheBench.\n"); - printf("Benchmarking %s (be patient)...\n", parsed_url.hostname); + printf("%sabr%s %s%d requests, concurrency %d%s\n", + STYLE_BOLD, STYLE_RESET, STYLE_DIM, total_requests, concurrency, STYLE_RESET); + printf("%s%s%s\n", STYLE_DIM, url, STYLE_RESET); if (insecure && strcmp(parsed_url.scheme, "https") == 0) { - printf("%sWarning: SSL certificate verification disabled%s\n", STYLE_YELLOW, STYLE_RESET); + printf("%scertificate verification disabled%s\n", STYLE_YELLOW, STYLE_RESET); } RequestResult *results = calloc((size_t)total_requests, sizeof(RequestResult)); @@ -739,6 +852,9 @@ int main(int argc, char *argv[]) { conn->request_index = requests_initiated; clock_gettime(CLOCK_MONOTONIC, &conn->start_time); + results[requests_initiated].start_offset_ms = + (conn->start_time.tv_sec - benchmark_start_time.tv_sec) * 1000.0 + + (conn->start_time.tv_nsec - benchmark_start_time.tv_nsec) / 1e6; char request[4096]; int req_len = snprintf(request, sizeof(request), @@ -1036,38 +1152,8 @@ int main(int argc, char *argv[]) { 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); + draw_progress(results, requests_completed, total_requests, + &benchmark_start_time, ¤t_time); release_connection(conn, keep_alive, &active_connections); } @@ -1100,8 +1186,7 @@ connection_closed: 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); + if (g_interactive) printf("\n"); print_summary(results, requests_completed, total_duration, url, concurrency, total_connections_made);