// 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); }