perf: add ghostty-bench and bring terminal throughput to Zig parity

Adds crates/ghostty-bench, a port of ZIG/src/benchmark and src/synthetic:
the ghostty-bench binary (terminal-stream, terminal-parser,
codepoint-width, grapheme-break, is-symbol, osc-parser, screen-clone
with Zig's CLI) and ghostty-synthetic data generators (+ascii, +utf8,
+osc, plus a Rust-only +sgr mode).

Measured against Ghostty's own ghostty-bench built with Zig 0.15.2
(ReleaseFast) vs --profile production, the hot paths were optimized
without behaviour changes:
- Terminal::print single-lookup fast path for width-1 chars mid-line
- page grapheme allocator: bitmap first-fit instead of a linear scan
  (was 38% of utf8 stream time; randomized equivalence test added)
- plain cells skip per-cell work in clear_cells and row cloning
- ASCII fast path + strict hand-written UTF-8 decoder (exhaustive
  equivalence test against std)
- parser/OSC parser split into inlined fast and state-change paths
- RenderState allocates grapheme vectors only for grapheme rows
- Page::assert_integrity no-op stub so the production profile builds

terminal-stream is now at parity (ascii 1.08x, utf8 0.97x, SGR 1.04x of
Zig's time). Still slower: screen-clone (~2.6x, zero-filling fresh
cell Vecs vs Zig's lazily-zeroed mmap), osc-parser (1.9x), and the raw
parser on SGR data (1.5x). Full table in docs/rewrite/notes/perf-P1.md.

Test Plan:
- cargo test --workspace (no failures)
- cargo run --profile production -p ghostty-bench -- +terminal-stream
  --data=<file> and compare with Zig's ghostty-bench
This commit is contained in:
ddidderr committed 2026-10-12 02:08:01 +02:00
1 parent c6a0c49567
commit 062c643ea8
18 files changed
+2195 -82

No files matched your search

+99 -5
View File
@@ -34,12 +34,45 @@ fn utf8_byte_sequence_length(first: u8) -> Option<usize> {
}
}
/// Decode `bytes` (exactly one sequence) strictly; `None` if ill-formed.
/// Decode `bytes` (exactly one sequence whose length matches its leading
/// byte) strictly; `None` if ill-formed (overlong, surrogate, out of range or
/// a bad continuation byte). Equivalent to `std::str::from_utf8` on one char.
#[inline]
fn utf8_decode(bytes: &[u8]) -> Option<u32> {
std::str::from_utf8(bytes)
.ok()
.and_then(|s| s.chars().next())
.map(|c| c as u32)
#[inline]
fn cont(b: u8) -> bool {
b & 0xC0 == 0x80
}
match *bytes {
[b0] if b0 < 0x80 => Some(u32::from(b0)),
[b0 @ 0xC2..=0xDF, b1] if cont(b1) => {
Some((u32::from(b0 & 0x1F) << 6) | u32::from(b1 & 0x3F))
}
[b0 @ 0xE0..=0xEF, b1, b2] if cont(b1) && cont(b2) => {
let ok = match b0 {
0xE0 => b1 >= 0xA0,
0xED => b1 < 0xA0,
_ => true,
};
ok.then(|| {
(u32::from(b0 & 0x0F) << 12) | (u32::from(b1 & 0x3F) << 6) | u32::from(b2 & 0x3F)
})
}
[b0 @ 0xF0..=0xF4, b1, b2, b3] if cont(b1) && cont(b2) && cont(b3) => {
let ok = match b0 {
0xF0 => b1 >= 0x90,
0xF4 => b1 < 0x90,
_ => true,
};
ok.then(|| {
(u32::from(b0 & 0x07) << 18)
| (u32::from(b1 & 0x3F) << 12)
| (u32::from(b2 & 0x3F) << 6)
| u32::from(b3 & 0x3F)
})
}
_ => None,
}
}
/// Decode UTF-8 from `input` into `output` until an ESC (0x1B) is found.
@@ -59,7 +92,17 @@ pub fn utf8_decode_until_control_seq(input: &[u8], output: &mut [u32]) -> Decode
let mut decode_offset = 0;
let mut decode_count = 0;
while decode_offset < decode.len() {
// ASCII fast path: a run of single-byte sequences.
let rem = &decode[decode_offset..];
if rem[0] < 0x80 {
let run = rem.iter().take_while(|&&b| b < 0x80).count();
for (o, &b) in output[decode_count..decode_count + run].iter_mut().zip(rem) {
*o = u32::from(b);
}
decode_count += run;
decode_offset += run;
continue;
}
let Some(cp_len) = utf8_byte_sequence_length(rem[0]) else {
break;
};
@@ -93,6 +136,57 @@ pub fn utf8_decode_until_control_seq(input: &[u8], output: &mut [u32]) -> Decode
mod tests {
use super::*;
#[test]
fn utf8_decode_matches_std() {
fn std_decode(bytes: &[u8]) -> Option<u32> {
std::str::from_utf8(bytes)
.ok()
.and_then(|s| s.chars().next())
.map(|c| c as u32)
}
for b0 in 0..=255u8 {
let Some(len) = utf8_byte_sequence_length(b0) else {
continue;
};
match len {
1 => assert_eq!(utf8_decode(&[b0]), std_decode(&[b0])),
2 => {
for b1 in 0..=255u8 {
assert_eq!(
utf8_decode(&[b0, b1]),
std_decode(&[b0, b1]),
"{b0:x} {b1:x}"
);
}
}
3 => {
for b1 in 0..=255u8 {
for b2 in 0..=255u8 {
assert_eq!(
utf8_decode(&[b0, b1, b2]),
std_decode(&[b0, b1, b2]),
"{b0:x} {b1:x} {b2:x}"
);
}
}
}
_ => {
for b1 in 0..=255u8 {
for b2 in [0x00, 0x7F, 0x80, 0xA0, 0xBF, 0xC0] {
for b3 in [0x00, 0x7F, 0x80, 0xBF, 0xC0] {
assert_eq!(
utf8_decode(&[b0, b1, b2, b3]),
std_decode(&[b0, b1, b2, b3]),
"{b0:x} {b1:x} {b2:x} {b3:x}"
);
}
}
}
}
}
}
}
#[test]
fn index_of_basic() {
assert_eq!(index_of(b"hello", b' '), None);