summaryrefslogtreecommitdiff
path: root/benchmarks
diff options
context:
space:
mode:
Diffstat (limited to 'benchmarks')
-rw-r--r--benchmarks/hist_bench.c93
-rw-r--r--benchmarks/meson.build13
-rw-r--r--benchmarks/term_resize_bench.c202
-rw-r--r--benchmarks/term_write_bench.c184
4 files changed, 492 insertions, 0 deletions
diff --git a/benchmarks/hist_bench.c b/benchmarks/hist_bench.c
new file mode 100644
index 0000000..2cf5608
--- /dev/null
+++ b/benchmarks/hist_bench.c
@@ -0,0 +1,93 @@
+#include "hist.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+static double now(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec + ts.tv_nsec * 1e-9;
+}
+
+/* Rows shaped like the term-write-bench lines that scroll into history. */
+static int make_row(const char *name, struct cell *row)
+{
+ struct cell d = { ' ', TERM_COLOR_FG, TERM_COLOR_BG, 0, 0 };
+ int n = 0;
+ if (!strcmp(name, "ascii-row")) {
+ const char *s = "The quick brown fox jumps over the lazy dog. 0123456789";
+ for (; s[n]; n++) {
+ row[n] = d;
+ row[n].c = (uint8_t)s[n];
+ }
+ } else if (!strcmp(name, "cjk-row")) {
+ for (int i = 0; i < 23; i++) {
+ row[n] = d;
+ row[n].c = 0x65e5 + (uint32_t)i;
+ row[n++].flags = CELL_WIDE;
+ row[n] = d;
+ row[n].c = 0;
+ row[n++].flags = CELL_TRAIL;
+ }
+ } else if (!strcmp(name, "colors-row")) {
+ const char *s = "red RGB";
+ for (; s[n]; n++) {
+ row[n] = d;
+ row[n].c = (uint8_t)s[n];
+ if (n < 3)
+ row[n].fg = term_color_index(1);
+ if (n > 3)
+ row[n].fg = term_color_rgb(12, 34, 56);
+ }
+ } else if (!strcmp(name, "fg-char-row")) {
+ for (; n < 120; n++) {
+ row[n] = d;
+ row[n].c = 'a' + (uint32_t)(n % 26);
+ row[n].fg = term_color_rgb((uint8_t)n, (uint8_t)(n * 3), (uint8_t)(n * 7));
+ }
+ }
+ return n;
+}
+
+int main(int argc, char **argv)
+{
+ int iterations = argc > 1 ? atoi(argv[1]) : 10000000;
+ struct hist h;
+ if (!term_hist_init(&h, 10000, 8 * 1024 * 1024)) {
+ fprintf(stderr, "failed to initialize hist\n");
+ return EXIT_FAILURE;
+ }
+
+ const char *line = "abcdefghijklmnopqrstuvwxyz\n";
+ int len = (int)strlen(line);
+ for (int i = 0; i < 10000; i++)
+ term_hist_push_ascii(&h, line, len, NULL, false);
+ double t0 = now();
+ for (int i = 0; i < iterations; i++)
+ term_hist_push_ascii(&h, line, len, NULL, false);
+ double elapsed = now() - t0;
+ printf("%-12s %6.2f ns/line %.2f GiB/s (%d B/line)\n", "ascii",
+ elapsed * 1e9 / iterations,
+ (double)iterations * len / (1024.0 * 1024.0 * 1024.0) / elapsed, len);
+ term_hist_free(&h);
+
+ static const char *rows[] = { "ascii-row", "cjk-row", "colors-row", "fg-char-row" };
+ for (size_t k = 0; k < sizeof(rows) / sizeof(rows[0]); k++) {
+ struct cell row[128];
+ int n = make_row(rows[k], row);
+ int iters = iterations / 4;
+ if (!term_hist_init(&h, 4096, 0))
+ return EXIT_FAILURE;
+ for (int i = 0; i < 10000; i++)
+ term_hist_push_row(&h, row, n, false, NULL);
+ t0 = now();
+ for (int i = 0; i < iters; i++)
+ term_hist_push_row(&h, row, n, false, NULL);
+ elapsed = now() - t0;
+ printf("%-12s %6.2f ns/row (%d cells)\n", rows[k], elapsed * 1e9 / iters, n);
+ term_hist_free(&h);
+ }
+ return EXIT_SUCCESS;
+}
diff --git a/benchmarks/meson.build b/benchmarks/meson.build
new file mode 100644
index 0000000..ef78f28
--- /dev/null
+++ b/benchmarks/meson.build
@@ -0,0 +1,13 @@
+foreach entry : [
+ ['term-write-bench', files('term_write_bench.c')],
+ ['term-resize-bench', files('term_resize_bench.c')],
+ ['hist-bench', files('hist_bench.c')],
+]
+ benchmark(entry[0],
+ executable(entry[0], entry[1],
+ c_args: ['-D_POSIX_C_SOURCE=200809L'],
+ dependencies: term_internal_dep,
+ build_by_default: false),
+ timeout: 600,
+ )
+endforeach
diff --git a/benchmarks/term_resize_bench.c b/benchmarks/term_resize_bench.c
new file mode 100644
index 0000000..08634d1
--- /dev/null
+++ b/benchmarks/term_resize_bench.c
@@ -0,0 +1,202 @@
+#include "term/term.h"
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define BUDGET_MS_DEFAULT 0.5
+/* Rows written past the history limit, so the history is full and wraps. */
+#define FILL_EXTRA_ROWS 512
+#define MAX_SAMPLES 4096
+
+/*
+ * History sizes to measure. 4096 is the size of the recorded baseline.
+ * The budget applies to every size. Today the larger sizes go over it; the
+ * terminal redesign targets a resize cost that does not grow with history.
+ */
+static const int history_sizes[] = { 100, 4096, 10000 };
+
+static double now(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec + ts.tv_nsec * 1e-9;
+}
+
+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;
+}
+
+/* Content mixes, chosen so reflow cost spans no-op to worst case. */
+enum content { SHORT, WRAPPED, MIXED, PROMPT, WIDE, CONTENT_COUNT };
+static const char *content_names[] = {"short", "wrapped", "mixed", "prompt", "wide"};
+
+static void emit_line(struct term *t, enum content kind, int n, int cols)
+{
+ char line[4096];
+ int len = 0;
+ switch (kind) {
+ case SHORT:
+ len = snprintf(line, sizeof(line), "%d: short output line\r\n", n);
+ break;
+ case WRAPPED:
+ /* ~3.5 screen rows at 120 cols: every resize must rewrap these. */
+ for (int i = 0; i < 420; i++) line[len++] = 'a' + (n + i) % 26;
+ line[len++] = '\r';
+ line[len++] = '\n';
+ break;
+ case MIXED:
+ if (n % 4 == 0)
+ for (int i = 0; i < 300; i++) line[len++] = 'a' + (n + i) % 26;
+ else
+ len = snprintf(line, sizeof(line), "%d: build step ok", n);
+ line[len++] = '\r';
+ line[len++] = '\n';
+ break;
+ case PROMPT:
+ /* OSC 133 marked prompt rows do not wrap; output rows do. */
+ if (n % 3 == 0)
+ len = snprintf(line, sizeof(line),
+ "\033]133;A\033\\user@host:~/very/long/project/path $ \033]133;B\033\\"
+ "cargo build --release --features=all\r\n");
+ else
+ len = snprintf(line, sizeof(line), " Compiling crate v0.%d.0\r\n", n % 100);
+ break;
+ case WIDE:
+ for (int i = 0; i < 60; i++) len += snprintf(line + len, sizeof(line) - len, "日本語");
+ line[len++] = '\r';
+ line[len++] = '\n';
+ break;
+ default:
+ break;
+ }
+ (void)cols;
+ term_feed(t, line, (size_t)len, 0);
+}
+
+static struct term *make_filled(enum content kind, int cols, int rows, int history)
+{
+ struct term *t = term_new(&(struct term_options){
+ .size = { .x = cols, .y = rows },
+ .scrollback = history,
+ });
+ if (!t) return NULL;
+ for (int n = 0; n < history + FILL_EXTRA_ROWS; n++)
+ emit_line(t, kind, n, cols);
+ return t;
+}
+
+static void resize(struct term *t, int cols, int rows)
+{
+ if (!term_resize(t, (struct term_vector2i){ .x = cols, .y = rows })) {
+ fprintf(stderr, "term_resize failed: out of memory\n");
+ exit(EXIT_FAILURE);
+ }
+}
+
+static void report(const char *content, const char *scenario, double *s, int n, double budget_ms)
+{
+ qsort(s, (size_t)n, sizeof(*s), cmp_double);
+ double p50 = s[n / 2];
+ double p99 = s[(int)((double)(n - 1) * 0.99)];
+ double max = s[n - 1];
+ printf("%-8s %-10s p50 %7.3f p99 %7.3f max %7.3f ms %s\n",
+ content, scenario, p50, p99, max, max <= budget_ms ? "ok" : "OVER");
+}
+
+/* One resize per freshly filled terminal: content never decays across samples. */
+static double bench_pair(enum content kind, int from_cols, int to_cols, int rows,
+ int history, double *out, int samples)
+{
+ double worst = 0;
+ for (int i = 0; i < samples; i++) {
+ struct term *t = make_filled(kind, from_cols, rows, history);
+ if (!t) exit(EXIT_FAILURE);
+ double start = now();
+ resize(t, to_cols, rows);
+ double ms = (now() - start) * 1e3;
+ term_free(t);
+ out[i] = ms;
+ if (ms > worst) worst = ms;
+ }
+ return worst;
+}
+
+/* A drag mutates progressively, so every step is timed on the same terminal. */
+static int bench_drag(enum content kind, int from_cols, int to_cols, int rows,
+ int history, double *out, int passes)
+{
+ int n = 0;
+ int step = from_cols > to_cols ? -1 : 1;
+ for (int p = 0; p < passes; p++) {
+ struct term *t = make_filled(kind, from_cols, rows, history);
+ if (!t) exit(EXIT_FAILURE);
+ for (int c = from_cols + step; c != to_cols + step; c += step) {
+ double start = now();
+ resize(t, c, rows);
+ out[n++] = (now() - start) * 1e3;
+ }
+ term_free(t);
+ }
+ return n;
+}
+
+int main(int argc, char **argv)
+{
+ const char *only = argc > 1 && strcmp(argv[1], "all") ? argv[1] : NULL;
+ double budget = argc > 2 ? strtod(argv[2], NULL) : BUDGET_MS_DEFAULT;
+ int only_history = argc > 3 ? atoi(argv[3]) : 0;
+ if (argc > 4 || only_history < 0) {
+ fprintf(stderr, "usage: %s [content|all [budget-ms [history]]]\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+ int rows = 40, samples = 32;
+ static const struct { const char *name; int from, to; } pairs[] = {
+ {"grow", 80, 120},
+ {"shrink", 120, 80},
+ {"double", 80, 160},
+ {"halve", 160, 80},
+ {"narrow", 120, 20},
+ {"widen", 20, 120},
+ };
+ static double s[MAX_SAMPLES];
+ bool matched = false, over = false;
+
+ for (size_t h = 0; h < sizeof(history_sizes) / sizeof(history_sizes[0]); h++) {
+ int history = history_sizes[h];
+ if (only_history && only_history != history) continue;
+ bool history_over = false;
+ printf("reflow budget %.3f ms, %d rows, scrollback %d\n\n", budget, rows, history);
+ for (int k = 0; k < CONTENT_COUNT; k++) {
+ if (only && strcmp(only, content_names[k])) continue;
+ matched = true;
+ for (size_t p = 0; p < sizeof(pairs) / sizeof(pairs[0]); p++) {
+ double worst = bench_pair((enum content)k, pairs[p].from, pairs[p].to, rows,
+ history, s, samples);
+ history_over |= worst > budget;
+ report(content_names[k], pairs[p].name, s, samples, budget);
+ }
+ int n = bench_drag((enum content)k, 120, 80, rows, history, s, 8);
+ for (int i = 0; i < n; i++) history_over |= s[i] > budget;
+ report(content_names[k], "drag", s, n, budget);
+ putchar('\n');
+ }
+ printf("scrollback %d: %s\n\n", history,
+ history_over ? "OVER budget" : "within budget");
+ over |= history_over;
+ }
+ if (!matched) {
+ if (only_history)
+ fprintf(stderr, "unknown content mix or history size: %s %d\n",
+ argv[1], only_history);
+ else
+ fprintf(stderr, "unknown content mix: %s\n", argv[1]);
+ return EXIT_FAILURE;
+ }
+ printf("%s\n", over ? "FAIL: at least one resize exceeded budget"
+ : "PASS: all resizes within budget");
+ return over ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/benchmarks/term_write_bench.c b/benchmarks/term_write_bench.c
new file mode 100644
index 0000000..50853f6
--- /dev/null
+++ b/benchmarks/term_write_bench.c
@@ -0,0 +1,184 @@
+#include "term/term.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+static double now(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec + ts.tv_nsec * 1e-9;
+}
+
+static int cmp_double(const void *a, const void *b)
+{
+ double d = *(const double *)a - *(const double *)b;
+ return (d > 0) - (d < 0);
+}
+
+/* Timed sample: iterations full-buffer writes split at raw byte offsets, so a
+ * chunk smaller than len cuts UTF-8/CSI sequences mid-stream and exercises the
+ * parser's save-and-resume path, like real PTY reads do. */
+static double run_sample(struct term *t, const char *buf, size_t len,
+ size_t chunk, int iterations, unsigned flags)
+{
+ double start = now();
+ for (int i = 0; i < iterations; i++) {
+ for (size_t pos = 0; pos < len; pos += chunk)
+ term_feed(t, buf + pos, len - pos < chunk ? len - pos : chunk, flags);
+ }
+ return now() - start;
+}
+
+/* The history size of the recorded baseline. Keep it, so results stay
+ * comparable with the reports in benchmarks/. */
+enum { BENCH_SCROLLBACK = 4096 };
+
+int main(int argc, char **argv)
+{
+ unsigned seed = argc > 2 ? (unsigned)strtoul(argv[2], NULL, 10) : 42;
+ size_t chunk = argc > 3 ? (size_t)strtoul(argv[3], NULL, 10) : 0;
+ int iterations = argc > 4 ? atoi(argv[4]) : 512;
+ int reps = argc > 5 ? atoi(argv[5]) : 7;
+ if (chunk > 65536 || iterations <= 0 || reps <= 0) {
+ fprintf(stderr, "usage: %s [case|all [seed [chunk (1..65536) [iterations (>0) [reps (>0)]]]]]\n"
+ "no chunk arg: each case runs whole-buffer and 4096-byte chunks\n",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+ static const struct { const char *name, *text; } cases[] = {
+ {"longline", NULL},
+ {"manyline", NULL},
+ {"lf-burst", NULL},
+ {"crlf-burst", NULL},
+ {"margin-lf", NULL},
+ {"margin-crlf", NULL},
+ {"ascii", "The quick brown fox jumps over the lazy dog. 0123456789\r\n"},
+ {"utf8", "Türkçe: İstanbul — αβγ 日本語\r\n"},
+ {"utf8var", "ab ér漢字 🙂 café x̂ 東京a\r\n"},
+ {"cjk", "日本語漢字仮名交じり文中文測試終端輸出性能比較\r\n"},
+ {"colors", "\033[31mred\033[0m \033[38;2;12;34;56mRGB\033[m\r\n"},
+ {"fg-char", "\033[38;2;1;23;145ma\033[38;2;23;145;2mb\033[38;2;145;2;34mc"},
+ {"fgbg-char", "\033[48;2;145;23;1m\033[38;2;1;23;145ma"
+ "\033[48;2;2;145;23m\033[38;2;23;145;2mb"
+ "\033[48;2;34;2;145m\033[38;2;145;2;34mc"},
+ {"fg-rand", NULL},
+ {"fgbg-rand", NULL},
+ {"cursor", "\033[12;20Hhello\033[2DXY\033[K\033[1A\033[3C!\r\n"},
+ {"erase", "\033[Hscreen\033[2J\033[Hnext\033[2K\r\n"},
+ {"dch", "some text on a row\033[10D\033[4Ptail\r\n"},
+ {"editor", NULL},
+ {"random", NULL},
+ };
+ char buf[65536];
+ double samples[64];
+ if (reps > (int)(sizeof(samples) / sizeof(samples[0])))
+ reps = sizeof(samples) / sizeof(samples[0]);
+ bool matched = false, header = false;
+ for (size_t c = 0; c < sizeof(cases) / sizeof(cases[0]); c++) {
+ if (argc > 1 && strcmp(argv[1], "all") && strcmp(argv[1], cases[c].name)) continue;
+ matched = true;
+ size_t len = sizeof(buf);
+ if (cases[c].text) {
+ size_t n = strlen(cases[c].text);
+ len = sizeof(buf) / n * n;
+ for (size_t i = 0; i < len; i += n)
+ memcpy(buf + i, cases[c].text, n);
+ } else if (!strncmp(cases[c].name, "fg", 2)) {
+ /* Random truecolor per character, like termbench's FGPerChar and
+ * FGBGPerChar. Repeating a few colors would let the branch
+ * predictor learn the digit-count branches. */
+ bool bg = !strncmp(cases[c].name, "fgbg", 4);
+ unsigned state = seed;
+ size_t n = 0;
+ for (;;) {
+ char seq[64];
+ int k = 0;
+ state = state * 1103515245u + 12345u;
+ unsigned r = (state >> 8) & 255, g = (state >> 16) & 255, b = (state >> 24) & 255;
+ if (bg)
+ k += snprintf(seq + k, sizeof(seq) - (size_t)k, "\033[48;2;%u;%u;%um", b, r, g);
+ k += snprintf(seq + k, sizeof(seq) - (size_t)k, "\033[38;2;%u;%u;%um%c", r, g, b,
+ 'a' + (int)(state % 26));
+ if (n + (size_t)k > sizeof(buf)) break;
+ memcpy(buf + n, seq, (size_t)k);
+ n += (size_t)k;
+ }
+ len = n;
+ } else if (strstr(cases[c].name, "-burst") ||
+ !strncmp(cases[c].name, "margin-", 7)) {
+ bool crlf = strstr(cases[c].name, "crlf") != NULL;
+ for (size_t i = 0; i < len; i++)
+ buf[i] = crlf && (i & 1) == 0 ? '\r' : '\n';
+ } else if (!strcmp(cases[c].name, "editor")) {
+ /* Full-screen redraw: CUP to each row followed by a full line of
+ * text, like a vim/htop frame. */
+ size_t n = 0;
+ for (int r = 1; r <= 40 && n + 140 < sizeof(buf); r++) {
+ n += (size_t)snprintf(buf + n, sizeof(buf) - n, "\033[%d;1H", r);
+ memset(buf + n, 'a' + r % 26, 110);
+ n += 110;
+ }
+ len = n;
+ } else {
+ unsigned state = seed;
+ unsigned choices = !strcmp(cases[c].name, "manyline") ? 27 : 26;
+ bool random = !strcmp(cases[c].name, "random");
+ for (size_t i = 0; i < len; i++) {
+ state = state * 1103515245u + 12345u;
+ unsigned pick = (state >> 16) % choices;
+ buf[i] = random ? (char)(state >> 16) : pick == 26 ? '\n' : (char)('a' + pick);
+ }
+ }
+ if (!header) {
+ header = true;
+ /* MiB/s counts input bytes: bytes/cell and work/byte differ per
+ * case, so only compare rows sharing case and chunk size. */
+ printf("# %-9s %6s %10s %10s %10s %8s\n",
+ "case", "chunk", "MiB/s med", "min", "max", "ns/B");
+ }
+ size_t chunks[2] = {chunk, 4096};
+ int nchunks = 1;
+ if (!chunk) {
+ chunks[0] = len;
+ nchunks = 2;
+ }
+ for (int k = 0; k < nchunks; k++) {
+ size_t ck = chunks[k];
+ const char *env_cols = getenv("TERM_COLS");
+ int cols_val = env_cols ? atoi(env_cols) : 120;
+ struct term *t = term_new(&(struct term_options){
+ .size = { .x = cols_val, .y = 40 },
+ .scrollback = BENCH_SCROLLBACK,
+ });
+ if (!t) return EXIT_FAILURE;
+ /* manyline has bare LFs: each one also returns the cursor to
+ * column 0, as a tty with ONLCR does. */
+ unsigned flags = !strcmp(cases[c].name, "manyline") ? TERM_FEED_ONLCR : 0;
+ if (!strncmp(cases[c].name, "margin-", 7)) {
+ const char setup[] = "\033[5;36r\033[36;1H";
+ term_feed(t, setup, sizeof(setup) - 1, 0);
+ } else if (strstr(cases[c].name, "-burst")) {
+ const char setup[] = "\033[40;1H";
+ term_feed(t, setup, sizeof(setup) - 1, 0);
+ }
+ /* Untimed warmup reaches steady-state scroll so rep 1 isn't a
+ * cold outlier. */
+ run_sample(t, buf, len, ck, 16, flags);
+ for (int r = 0; r < reps; r++)
+ samples[r] = run_sample(t, buf, len, ck, iterations, flags);
+ qsort(samples, reps, sizeof(samples[0]), cmp_double);
+ double bytes = (double)len * iterations;
+ double med = bytes / (1024 * 1024) / samples[reps / 2];
+ double min = bytes / (1024 * 1024) / samples[reps - 1];
+ double max = bytes / (1024 * 1024) / samples[0];
+ printf("%-9s %6zu %10.2f %10.2f %10.2f %8.3f\n",
+ cases[c].name, ck, med, min, max,
+ samples[reps / 2] * 1e9 / bytes);
+ term_free(t);
+ }
+ }
+ if (!matched) fprintf(stderr, "unknown benchmark case: %s\n", argv[1]);
+ return matched ? EXIT_SUCCESS : EXIT_FAILURE;
+}