220 lines
8.1 KiB
Rust
220 lines
8.1 KiB
Rust
//! VT parser throughput benchmark.
|
||
//!
|
||
//! Measures how quickly the terminal can process raw bytes from a PTY stream.
|
||
//! Methodology is modelled on Kitty's `kitten __benchmark__` (without `--render`):
|
||
//! 1. Generate representative synthetic data for each scenario.
|
||
//! 2. Do one warmup pass.
|
||
//! 3. Run `REPETITIONS` timed passes, report MB/s and elapsed time.
|
||
//!
|
||
//! Run with:
|
||
//! cargo run --bin bench_process --release
|
||
|
||
use std::time::Instant;
|
||
use winiterm::terminal::Terminal;
|
||
use winiterm::vt_parser::Parser;
|
||
|
||
const REPETITIONS: usize = 100;
|
||
|
||
// ─── Data generators ─────────────────────────────────────────────────────────
|
||
|
||
const ASCII_PRINTABLE: &[u8] =
|
||
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
||
const CONTROL_CHARS: &[u8] = b"\n\t";
|
||
|
||
/// Simple linear-congruential PRNG (deterministic, no dependency).
|
||
#[inline]
|
||
fn lcg(rng: &mut u64) -> u64 {
|
||
*rng = rng
|
||
.wrapping_mul(6_364_136_223_846_793_005)
|
||
.wrapping_add(1_442_695_040_888_963_407);
|
||
*rng
|
||
}
|
||
|
||
/// Pick a random byte from `alphabet`.
|
||
fn random_byte(rng: &mut u64, alphabet: &[u8]) -> u8 {
|
||
alphabet[(lcg(rng) >> 33) as usize % alphabet.len()]
|
||
}
|
||
|
||
/// Generate `len` random bytes from the combined ASCII + control alphabet.
|
||
fn random_text(len: usize, rng: &mut u64) -> Vec<u8> {
|
||
let alphabet: Vec<u8> = ASCII_PRINTABLE
|
||
.iter()
|
||
.chain(CONTROL_CHARS.iter())
|
||
.copied()
|
||
.collect();
|
||
(0..len).map(|_| random_byte(rng, &alphabet)).collect()
|
||
}
|
||
|
||
// ─── Benchmark runner ─────────────────────────────────────────────────────────
|
||
|
||
fn run_benchmark<F>(name: &str, data: &[u8], repetitions: usize, setup: F)
|
||
where
|
||
F: Fn() -> (Terminal, Parser),
|
||
{
|
||
// Warmup
|
||
{
|
||
let (mut term, mut parser) = setup();
|
||
parser.parse(data, &mut term);
|
||
}
|
||
|
||
// Timed passes
|
||
let start = Instant::now();
|
||
for _ in 0..repetitions {
|
||
let (mut term, mut parser) = setup();
|
||
parser.parse(data, &mut term);
|
||
}
|
||
let elapsed = start.elapsed();
|
||
|
||
let total_bytes = data.len() * repetitions;
|
||
let mb = total_bytes as f64 / 1_048_576.0;
|
||
let rate = mb / elapsed.as_secs_f64();
|
||
|
||
println!(
|
||
" {:<30} : {:>6.2}s {:>8.1} MB/s ({} reps × {:.2} MB)",
|
||
name,
|
||
elapsed.as_secs_f64(),
|
||
rate,
|
||
repetitions,
|
||
data.len() as f64 / 1_048_576.0,
|
||
);
|
||
}
|
||
|
||
fn make_terminal() -> (Terminal, Parser) {
|
||
(Terminal::new(80, 25, 20_000), Parser::new())
|
||
}
|
||
|
||
// ─── main ─────────────────────────────────────────────────────────────────────
|
||
|
||
fn main() {
|
||
println!("=== winiterm VT Parser Benchmark ===");
|
||
println!("(methodology matches Kitty's kitten __benchmark__ without --render)\n");
|
||
|
||
// ── 1. Plain ASCII text ───────────────────────────────────────────────────
|
||
println!("--- Plain ASCII text ---");
|
||
{
|
||
let target = 1024 * 2048 + 13;
|
||
let mut rng: u64 = 0xDEAD_BEEF;
|
||
let data = random_text(target, &mut rng);
|
||
run_benchmark("ascii_only", &data, REPETITIONS, make_terminal);
|
||
}
|
||
|
||
// ── 2. CSI escape codes interleaved with text ─────────────────────────────
|
||
println!("\n--- CSI codes with text ---");
|
||
{
|
||
let target = 1024 * 1024 + 17;
|
||
let mut rng: u64 = 0x1234_5678;
|
||
let mut data: Vec<u8> = Vec::with_capacity(target + 128);
|
||
|
||
while data.len() < target {
|
||
match (lcg(&mut rng) >> 33) % 10 {
|
||
0 => {
|
||
// Plain text burst (1–72 chars)
|
||
let len = ((lcg(&mut rng) >> 33) % 72 + 1) as usize;
|
||
data.extend(random_text(len, &mut rng));
|
||
}
|
||
1 | 2 => {
|
||
// Cursor movement + reset
|
||
data.extend_from_slice(b"\x1b[m\x1b[?1h\x1b[H");
|
||
}
|
||
3 => {
|
||
// SGR: bold + italic + colours
|
||
data.extend_from_slice(b"\x1b[1;3;31;42m");
|
||
}
|
||
4 => {
|
||
// SGR: 256-colour foreground + background
|
||
data.extend_from_slice(b"\x1b[38;5;214;48;5;236m");
|
||
}
|
||
5 => {
|
||
// SGR: true-colour (RGB) foreground
|
||
data.extend_from_slice(b"\x1b[38;2;255;128;0m");
|
||
}
|
||
6 | 7 => {
|
||
// Cursor movement + erase
|
||
data.extend_from_slice(b"\x1b[m\x1b[5A\x1b[2K\x1b[1J");
|
||
}
|
||
_ => {
|
||
// SGR reset + misc cursor
|
||
data.extend_from_slice(b"\x1b[0m\x1b[10;20H\x1b[?25h");
|
||
}
|
||
}
|
||
}
|
||
data.extend_from_slice(b"\x1b[m");
|
||
run_benchmark("csi_with_text", &data, REPETITIONS, make_terminal);
|
||
}
|
||
|
||
// ── 3. Long OSC sequences (window titles) ────────────────────────────────
|
||
println!("\n--- Long OSC sequences ---");
|
||
{
|
||
let title_body: String = (0..512)
|
||
.map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char)
|
||
.collect();
|
||
let mut data: Vec<u8> = Vec::new();
|
||
for _ in 0..2048 {
|
||
data.extend_from_slice(b"\x1b]2;");
|
||
data.extend_from_slice(title_body.as_bytes());
|
||
data.push(0x07); // BEL terminator
|
||
}
|
||
run_benchmark("osc_long_titles", &data, REPETITIONS, make_terminal);
|
||
}
|
||
|
||
// ── 4. Dense colour changes (typical syntax-highlighted code output) ──────
|
||
println!("\n--- Dense colour output (syntax highlighting) ---");
|
||
{
|
||
let mut data: Vec<u8> = Vec::new();
|
||
let words = ["fn", "main", "let", "mut", "if", "return", "use", "pub"];
|
||
let mut rng: u64 = 0xABCD_EF01;
|
||
for _ in 0..8192 {
|
||
// Random foreground colour (30–45)
|
||
let fg = 30 + (lcg(&mut rng) >> 33) % 16;
|
||
let word = words[(lcg(&mut rng) >> 33) as usize % words.len()];
|
||
let seq = format!("\x1b[{fg}m{word} \x1b[m");
|
||
data.extend_from_slice(seq.as_bytes());
|
||
}
|
||
run_benchmark("dense_color_output", &data, REPETITIONS, make_terminal);
|
||
}
|
||
|
||
// ── 5. Mixed: scroll + insert/delete lines ────────────────────────────────
|
||
println!("\n--- Scroll-heavy output ---");
|
||
{
|
||
let mut data: Vec<u8> = Vec::new();
|
||
let mut rng: u64 = 0xFEED_FACE;
|
||
for _ in 0..4096 {
|
||
data.extend(random_text(60, &mut rng));
|
||
data.push(b'\n');
|
||
// Occasionally insert/delete lines
|
||
if (lcg(&mut rng) >> 33) % 8 == 0 {
|
||
data.extend_from_slice(b"\x1b[1L"); // insert line
|
||
}
|
||
}
|
||
run_benchmark("scroll_heavy", &data, REPETITIONS, make_terminal);
|
||
}
|
||
|
||
println!("\n=== Benchmark complete ===");
|
||
println!("Note: measures CPU parse + terminal state updates only (no GPU rendering).");
|
||
println!("Compare against Kitty: kitten __benchmark__ (without --render flag)");
|
||
}
|
||
|
||
// ─── Minimal `write!` support for building data vecs ─────────────────────────
|
||
|
||
use std::fmt::Write as FmtWrite;
|
||
|
||
struct VecWriter<'a>(&'a mut Vec<u8>);
|
||
|
||
impl FmtWrite for VecWriter<'_> {
|
||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||
self.0.extend_from_slice(s.as_bytes());
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
// Override the `write!` macro to work with our VecWriter.
|
||
trait WriteVec {
|
||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>);
|
||
}
|
||
|
||
impl WriteVec for Vec<u8> {
|
||
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) {
|
||
let _ = std::fmt::write(&mut VecWriter(self), args);
|
||
}
|
||
}
|