summaryrefslogtreecommitdiff
path: root/benchmarks/term_write_bench.c
blob: 7649ed836befe3d367eb8ebee00fd51e40b3705a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#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) {
                fprintf(stderr, "term_new failed: TERM_COLS must be 0 to %d\n", TERM_MAX_COLS);
                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;
}