This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch master
in repository terminology.
View the commit online.
commit 28d411811ccd770159141b45b712d6c3b4db9b8c
Author: Boris Faure <[email protected]>
AuthorDate: Sat Aug 22 21:31:11 2026 +0200
tests/bench: generate the corpora and register the benchmark
---
tests/bench/gen_corpus.py | 174 ++++++++++++++++++++++++++++++++++++++++++++++
tests/meson.build | 26 +++++++
2 files changed, 200 insertions(+)
diff --git a/tests/bench/gen_corpus.py b/tests/bench/gen_corpus.py
new file mode 100644
index 00000000..e2364740
--- /dev/null
+++ b/tests/bench/gen_corpus.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+"""Generate deterministic benchmark corpora for tybench.
+
+Four workloads, chosen so that a change to one part of the intake path shows up
+in one column rather than being averaged away:
+
+ plain-ascii long lines of printable ASCII, almost no escapes. The pure
+ text-append path -- what an ASCII fast path targets.
+ scroll short lines, so the cost is dominated by line feeds, wrapping and
+ backlog pushes rather than by cell writes.
+ sgr colour-saturated output. Exercises the CSI parser and its
+ parameter scanning instead of the text path.
+ unicode CJK, emoji, combining marks and accented Latin. Exercises
+ multibyte decode, double-width handling, and the paths a
+ byte-space fast path has to bail out of correctly.
+
+Output is byte-identical across runs (fixed seed, no clock, no locale) so that
+numbers from different days are comparable.
+"""
+
+import argparse
+import os
+import random
+
+TARGET_DEFAULT = 4 * 1024 * 1024
+
+WORDS = (
+ "terminal escape sequence parser buffer cursor render glyph column row "
+ "codepoint attribute palette scrollback viewport selection backlog cell "
+ "unicode decode dispatch throughput latency kernel syscall pipeline vector"
+).split()
+
+
+def _rng(tag):
+ # Per-corpus seed: adding a corpus cannot change the bytes of another.
+ return random.Random("terminology-bench:" + tag)
+
+
+def gen_plain_ascii(target):
+ """Long lines of printable ASCII. Maximises consecutive printable runs."""
+ rng = _rng("plain-ascii")
+ out = bytearray()
+ while len(out) < target:
+ line = []
+ width = 0
+ # Aim well past the 80-column screen so wrapping is exercised too.
+ while width < 100:
+ w = rng.choice(WORDS)
+ line.append(w)
+ width += len(w) + 1
+ out += (" ".join(line) + "\n").encode("ascii")
+ return bytes(out[:target])
+
+
+def gen_scroll(target):
+ """Short lines: one newline every few bytes, so scrolling dominates."""
+ rng = _rng("scroll")
+ out = bytearray()
+ n = 0
+ while len(out) < target:
+ out += ("%6d %s\n" % (n, rng.choice(WORDS))).encode("ascii")
+ n += 1
+ return bytes(out[:target])
+
+
+def gen_sgr(target):
+ """Colour-heavy output: an SGR sequence for nearly every short text run."""
+ rng = _rng("sgr")
+ out = bytearray()
+ cut = 0
+ while len(out) < target:
+ style = rng.choice(
+ [
+ "\033[%dm" % rng.randint(30, 37),
+ "\033[1;%dm" % rng.randint(30, 37),
+ "\033[38;5;%dm" % rng.randint(0, 255),
+ "\033[48;5;%dm" % rng.randint(0, 255),
+ "\033[38;2;%d;%d;%dm"
+ % (rng.randint(0, 255), rng.randint(0, 255), rng.randint(0, 255)),
+ "\033[0m",
+ "\033[1m",
+ "\033[4m",
+ ]
+ )
+ out += (style + rng.choice(WORDS)).encode("ascii")
+ if rng.random() < 0.15:
+ out += b"\033[0m\n"
+ # Remember where a cut may fall. Truncating at an arbitrary byte leaves
+ # half an escape sequence at the end, and since each pass restarts at
+ # the top of the corpus, that half runs into the leading ESC [ of the
+ # next pass -- an ESC [ [ the parser rightly complains about.
+ if len(out) <= target:
+ cut = len(out)
+ # A target smaller than the first sequence still gets whole sequences.
+ if cut == 0:
+ cut = len(out)
+ # End reset, so a pass cannot leave attributes set for the pass after it.
+ return bytes(out[:cut]) + b"\033[0m\n"
+
+
+def gen_unicode(target):
+ """Mixed multibyte: 2-, 3- and 4-byte sequences plus combining marks."""
+ rng = _rng("unicode")
+ pools = [
+ "éèêüñåøæ", # 2-byte Latin-1
+ "你好世界漢字日本", # 3-byte CJK, wide
+ "αβγδЖДЯш", # 2-byte Greek/Cyrillic
+ "\U0001f600\U0001f680\U0001f4a1\U0001f30d", # 4-byte emoji
+ ]
+ out = bytearray()
+ while len(out) < target:
+ line = []
+ for _ in range(rng.randint(8, 20)):
+ pool = rng.choice(pools)
+ chunk = "".join(rng.choice(pool) for _ in range(rng.randint(1, 6)))
+ # Sprinkle combining acute accents onto some Latin runs.
+ if pool is pools[0] and rng.random() < 0.3:
+ chunk += "́"
+ line.append(chunk)
+ # Interleave ASCII so the corpus exercises transitions in and out of the
+ # multibyte path rather than staying in one mode.
+ line.append(rng.choice(WORDS))
+ out += (" ".join(line) + "\n").encode("utf-8")
+ # Never truncate mid-sequence: that would make the corpus itself invalid.
+ data = ""
+ # Whole lines are appended, so the buffer already ends on a complete
+ # character; if the last one landed exactly on the target there is no byte
+ # past it to inspect and nothing to trim.
+ if target >= len(data):
+ return data
+ cut = target
+ while cut > 0 and (data[cut] & 0xC0) == 0x80:
+ cut -= 1
+ return data[:cut]
+
+
+GENERATORS = {
+ "plain-ascii": gen_plain_ascii,
+ "scroll": gen_scroll,
+ "sgr": gen_sgr,
+ "unicode": gen_unicode,
+}
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("outdir", help="directory to write corpora into")
+ ap.add_argument("-s", "--size", type=int, default=TARGET_DEFAULT,
+ help="approximate bytes per corpus (default %d)" % TARGET_DEFAULT)
+ ap.add_argument("-o", "--only", action="" choices=sorted(GENERATORS),
+ help="generate only this corpus (repeatable)")
+ args = ap.parse_args()
+
+ os.makedirs(args.outdir, exist_ok=True)
+ names = args.only if args.only else sorted(GENERATORS)
+
+ for name in names:
+ data = ""
+ path = os.path.join(args.outdir, name)
+ # Skip the rewrite if content is already correct, so timestamps stay put
+ # and build systems do not re-run downstream steps for nothing.
+ if os.path.exists(path):
+ with open(path, "rb") as f:
+ if f.read() == data:
+ print("%-14s %9d bytes (unchanged)" % (name, len(data)))
+ continue
+ with open(path, "wb") as f:
+ f.write(data)
+ print("%-14s %9d bytes" % (name, len(data)))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/meson.build b/tests/meson.build
index 8c36669c..25ec8267 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -31,3 +31,29 @@ if tests
# The in-process C unit tests compiled into tytest itself.
test('unit', tytest, args: ['all'], timeout: 120)
endif
+
+if benchmarks
+ python3 = find_program('python3')
+ bench_corpus_dir = meson.current_build_dir()
+
+ # Generated rather than committed: megabytes of derived data, and the
+ # generator is deterministic so the bytes are reproducible. It writes to
+ # @OUTDIR@ so that what it produces is what 'output' declares.
+ bench_corpus = custom_target('bench-corpus',
+ output: ['plain-ascii', 'scroll', 'sgr', 'unicode'],
+ command: [python3,
+ meson.current_source_dir() / 'bench' / 'gen_corpus.py',
+ '@OUTDIR@'],
+ build_by_default: false)
+
+ # Run with `meson test --benchmark`.
+ benchmark('pty-intake',
+ tybench,
+ args: ['-i', '5',
+ bench_corpus_dir / 'plain-ascii',
+ bench_corpus_dir / 'scroll',
+ bench_corpus_dir / 'sgr',
+ bench_corpus_dir / 'unicode'],
+ depends: bench_corpus,
+ timeout: 600)
+endif
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.