summaryrefslogtreecommitdiff
path: root/benchmarks/term_resize_bench.c
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /benchmarks/term_resize_bench.c
term: move the term core to subprojects/term
The core now builds, tests, and benchmarks alone, as a Meson subproject with no dependency on the main tree. - include/term/ holds term.h and the header-only unicode/. src/ holds the core and its private headers. The main tree gets only include/term through term_dep, so it cannot reach a private header. - The term tests, the term benchmarks, the Kitty parser suite, and tools/amalgamate.py and tools/generate_unicode.py move with it. The tests run as term:<name>. - The main build passes its -march (arch_flag) and b_ndebug to the subproject. A directory configured before this commit needs meson configure -Dterm:arch_flag=-march=... once. - The term tests compile with -UNDEBUG. They do work inside assert. - The subproject exports term_test_dep with term_api.h, for the main tree's render, thumbnail, color, and selection tests. - The Kitty bridge used struct vector2i and has not compiled since the public API redesign. It builds again. 2 of its 14 tests fail: REP after a tab, and a cursor report after a resize. - dirgraph.py drops the term and unicode layers. Its self-test uses pty. - The symbols test ignores the __odr_asan symbols that ASan adds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'benchmarks/term_resize_bench.c')
-rw-r--r--benchmarks/term_resize_bench.c202
1 files changed, 202 insertions, 0 deletions
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;
+}