diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 17:49:34 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-12 17:49:34 +0300 |
| commit | 4384435941b8c7658856b381d071aee2e0035e89 (patch) | |
| tree | 5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /benchmarks | |
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')
| -rw-r--r-- | benchmarks/hist_bench.c | 93 | ||||
| -rw-r--r-- | benchmarks/meson.build | 13 | ||||
| -rw-r--r-- | benchmarks/term_resize_bench.c | 202 | ||||
| -rw-r--r-- | benchmarks/term_write_bench.c | 184 |
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; +} |
