feat: rebuild the terminal output around a single column-chart renderer
Build and Test / build-c (push) Successful in 45s
Build and Test / build-python (push) Successful in 39s
Build and Test / valgrind (push) Successful in 42s

The visualization was six unrelated widgets - a histogram, a timeline, an
error-rate bar, percentile bars, a fill gauge, plus a dashboard, heatmap and
"real-time monitor" charting simulated CPU/memory/disk data. Colour was
decorative and inconsistent, and the result did not read as a benchmark tool.

Replace all of it with one drawing primitive: a vertical column chart with a
zero-based labelled y-axis, a baseline and a labelled x-axis. Four charts share
that shape - throughput over time (failures stacked in red), response time over
time, response time distribution, and response time percentiles. Colour now
carries meaning only: cyan is data, amber is the tail (p95+), red is failure.
Charts size themselves to the terminal width.

The summary keeps every number it had, laid out as aligned key/value blocks
under section rules, with the ApacheBench percentile table as a 3-column grid.

Fixes found along the way:

- "Latency over the run" plotted the sorted durations, so it was always a
  monotonic ramp rather than a timeline. Requests now record a start offset
  relative to the run, so the time-axis charts show what actually happened.
- The throughput gauge was scaled against a hardcoded 1000.0 req/s, so it read
  a near-zero percentage regardless of the target.
- The live progress line recomputed statistics over every completed request on
  every completion, an O(n^2) cost inside the benchmark loop. It is now
  throttled to ~20 Hz and skipped entirely when stdout is not a terminal.

Colour and the progress bar are gated on isatty(), so redirected output is
plain, greppable text with no escape codes.

A heavy tail (p50 57 ms, max 1525 ms) previously collapsed the distribution
into a single column. The chart now clips its axis at p95 when the maximum
dwarfs it and states so beneath the chart; well-behaved runs are never clipped
and the full range stays in the footer either way.

Also drop 77 lines of dead simulation code from main.c, wire graphs.c into the
Makefile, untrack the built binary, and add a .gitignore.

Builds warning-free at -Wall -Wextra; valgrind clean on the loaded,
single-request and all-failed paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCMSMcgcwnH9a8hhaLsg6j
This commit is contained in:
2026-09-08 06:43:57 +00:00
co-authored by Claude Opus 5
parent 95ffdee251
commit 6c830181c8
7 changed files with 638 additions and 77 deletions
+10
View File
@@ -0,0 +1,10 @@
# Build artifacts
abr
*.o
# Tool logs
dpc.log
# Python
__pycache__/
*.py[cod]
+7 -4
View File
@@ -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
+17
View File
@@ -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 <requests> -c <concurrency> [-k] [-i] <url>
```
### 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.
BIN
View File
Binary file not shown.
+408
View File
@@ -0,0 +1,408 @@
// retoor <retoor@molodetz.nl>
#include "graphs.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
/* 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);
}
+38
View File
@@ -0,0 +1,38 @@
// retoor <retoor@molodetz.nl>
#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
+158 -73
View File
@@ -20,13 +20,28 @@
#include <openssl/err.h>
#include <stdbool.h>
#include <ctype.h>
#include <stdarg.h>
#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,36 +656,67 @@ 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, &current_time);
double elapsed_time = (current_time.tv_sec - benchmark_start_time.tv_sec) +
(current_time.tv_nsec - benchmark_start_time.tv_nsec) / 1e9;
double req_per_sec = elapsed_time > 0 ? requests_completed / elapsed_time : 0;
double avg_latency_ms = success_count > 0 ? total_duration_ms / success_count : 0;
double transfer_rate_kbs = elapsed_time > 0 ? (total_bytes_transferred / 1024.0) / elapsed_time : 0;
const char *fail_color = (failed_count == 0) ? STYLE_GREEN : STYLE_RED;
fprintf(stdout, "\r%sCompleted: %d/%d | Failed: %s%d%s%s | RPS: %s%.1f%s%s | Avg Latency: %.0fms | Rate: %.1f KB/s%s",
STYLE_BOLD, requests_completed, total_requests,
fail_color, failed_count, STYLE_RESET, STYLE_BOLD,
STYLE_GREEN, req_per_sec, STYLE_RESET, STYLE_BOLD,
avg_latency_ms, transfer_rate_kbs, STYLE_RESET);
fflush(stdout);
draw_progress(results, requests_completed, total_requests,
&benchmark_start_time, &current_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);