Files
ghostty-rs/crates/ghostty-bench/src/benches.rs
T
ddidderr f9b333545a fix: last RV11 gaps - bench enum args, theme NUL paths, Zig error names
Resolves the 3 remaining gaps in docs/rewrite/notes/review-rv11-codex.md
(final-Z14.md):
- ghostty-bench validates every occurrence of enum options in order
  (Args::take_enum; empty value resets) like cli/args.zig, so
  `--mode=bad --mode=noop` fails as in Zig.
- Theme paths with an interior NUL open the prefix like Zig's C-string
  path handling (verified against /usr/bin/ghostty).
- Filesystem diagnostics print Zig's error names (error.FileNotFound,
  NotDir, AccessDenied, IsDir, NameTooLong, SymLinkLoop, ...) via a
  shared ghostty_os::zig_error mapping used by theme/path/config-file/
  file_load/edit-config/list-themes; messages match the oracle.

Test Plan:
- cargo test --workspace --no-fail-fast
- just check-macos (both), just check-windows, just check-wasm
2026-10-12 13:54:15 +02:00

769 lines
22 KiB
Rust

//! The individual benchmarks (ports of `benchmark/*.zig`) and the CLI action
//! dispatch (`benchmark/cli.zig`).
use std::{hint::black_box, io::Read};
use ghostty_terminal::{
point::{Coordinate, Point},
render::RenderState,
screen_set::Key as ScreenKey,
terminal::{Options as TermOptions, Terminal},
};
use ghostty_vt::{
osc,
parser::Parser,
stream::{Action, Handler, Stream},
utf8_decoder::Utf8Decoder,
};
use crate::{
args::{Args, data_file, read_short},
benchmark::{Benchmark, BenchmarkFailed, Result, RunMode, RunResult, run},
};
const CHUNK: usize = 4096;
/// The available actions (benchmarks) of the CLI.
pub const ACTIONS: &[&str] = &[
"codepoint-width",
"grapheme-break",
"screen-clone",
"terminal-parser",
"terminal-stream",
"is-symbol",
"osc-parser",
];
fn warn(what: &str, e: &dyn std::fmt::Display) -> BenchmarkFailed {
log::warn!("{what} err={e}");
BenchmarkFailed
}
/// Shared data-file handling: opened in setup, closed in teardown.
struct Data {
path: Option<std::path::PathBuf>,
f: Option<Box<dyn Read>>,
}
impl Data {
fn new(path: Option<std::path::PathBuf>) -> Data {
Data { path, f: None }
}
fn open(&mut self) -> Result {
self.f =
data_file(self.path.as_deref()).map_err(|e| warn("error opening data file", &e))?;
Ok(())
}
/// Feed every 4096-byte chunk of the file to `func`.
fn for_each_chunk(&mut self, mut func: impl FnMut(&[u8]) -> Result) -> Result {
let Some(f) = self.f.as_mut() else {
return Ok(());
};
let mut buf = [0u8; CHUNK];
loop {
let n = read_short(f.as_mut(), &mut buf)
.map_err(|e| warn("error reading data file", &e))?;
if n == 0 {
return Ok(());
}
func(&buf[..n])?;
}
}
}
// ---------------------------------------------------------------------------
// terminal-stream
struct PrintHandler {
t: Terminal,
}
impl Handler for PrintHandler {
type Error = ghostty_terminal::terminal::Error;
#[inline]
fn vt(&mut self, action: Action<'_>) -> std::result::Result<(), Self::Error> {
if let Action::Print(p) = action {
self.t.print(p.cp)?;
}
Ok(())
}
}
pub struct TerminalStream {
data: Data,
stream: Stream<PrintHandler>,
/// Rust-only: use the full `stream_readonly` handler (applies every
/// terminal-modifying action) instead of the print-only handler.
full: bool,
}
impl TerminalStream {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let rows = args.take("terminal-rows", 80u16)?;
let cols = args.take("terminal-cols", 120u16)?;
let data = args.take_opt_path("data")?;
let full = args.take_enum("handler", false, &[("print", false), ("full", true)])?;
let t = Terminal::new(TermOptions::new(cols, rows)).map_err(|e| format!("{e:?}"))?;
Ok(TerminalStream {
data: Data::new(data),
stream: Stream::new(PrintHandler { t }),
full,
})
}
/// Total bytes processed so far is not tracked; see `data_len`.
pub fn terminal(&self) -> &Terminal {
&self.stream.handler.t
}
}
impl Benchmark for TerminalStream {
fn setup(&mut self) -> Result {
self.stream.handler.t.full_reset();
self.data.open()
}
fn teardown(&mut self) {
self.data.f = None;
}
fn step(&mut self) -> Result {
if self.full {
let mut s = self.stream.handler.t.vt_stream();
return self.data.for_each_chunk(|chunk| {
s.next_slice(chunk)
.map_err(|e| warn("error processing data file chunk", &format!("{e:?}")))
});
}
let stream = &mut self.stream;
self.data.for_each_chunk(|chunk| {
stream
.next_slice(chunk)
.map_err(|e| warn("error processing data file chunk", &format!("{e:?}")))
})
}
}
// ---------------------------------------------------------------------------
// terminal-parser
pub struct TerminalParser {
data: Data,
}
impl TerminalParser {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
Ok(TerminalParser {
data: Data::new(args.take_opt_path("data")?),
})
}
}
impl Benchmark for TerminalParser {
fn setup(&mut self) -> Result {
self.data.open()
}
fn teardown(&mut self) {
self.data.f = None;
}
fn step(&mut self) -> Result {
let mut p = Parser::new();
self.data.for_each_chunk(|chunk| {
for &c in chunk {
p.next_with(c, drop);
}
Ok(())
})
}
}
// ---------------------------------------------------------------------------
// osc-parser
pub struct OscParser {
data: Data,
parser: osc::Parser,
}
impl OscParser {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
Ok(OscParser {
data: Data::new(args.take_opt_path("data")?),
parser: osc::Parser::init(true),
})
}
}
impl Benchmark for OscParser {
fn setup(&mut self) -> Result {
self.data.open()?;
self.parser.reset();
Ok(())
}
fn teardown(&mut self) {
self.data.f = None;
}
fn step(&mut self) -> Result {
let Some(f) = self.data.f.as_mut() else {
return Ok(());
};
let mut osc_buf = [0u8; 4096];
loop {
let mut len_buf = [0u8; 8];
match read_short(f.as_mut(), &mut len_buf) {
Ok(8) => {}
Ok(_) => return Ok(()),
Err(_) => return Err(BenchmarkFailed),
}
let len = usize::from_le_bytes(len_buf);
if len > osc_buf.len() {
return Err(BenchmarkFailed);
}
match read_short(f.as_mut(), &mut osc_buf[..len]) {
Ok(n) if n == len => {}
Ok(_) => return Ok(()),
Err(_) => return Err(BenchmarkFailed),
}
self.parser.next_slice(&osc_buf[..len]);
black_box(self.parser.end(Some(0x07)));
self.parser.reset();
}
}
}
// ---------------------------------------------------------------------------
// codepoint-width
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WidthMode {
Noop,
Wcwidth,
Simd,
Table,
}
pub struct CodepointWidth {
data: Data,
mode: WidthMode,
}
impl CodepointWidth {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let mode = args.take_enum(
"mode",
WidthMode::Noop,
&[
("noop", WidthMode::Noop),
("wcwidth", WidthMode::Wcwidth),
("simd", WidthMode::Simd),
("table", WidthMode::Table),
],
)?;
Ok(CodepointWidth {
data: Data::new(args.take_opt_path("data")?),
mode,
})
}
}
/// Decode `chunk` and call `func` for each codepoint.
#[inline]
fn decode_each(d: &mut Utf8Decoder, chunk: &[u8], mut func: impl FnMut(u32)) {
for &c in chunk {
let (cp, consumed) = d.next(c);
debug_assert!(consumed);
if let Some(cp) = cp {
func(cp);
}
}
}
/// libc `wcwidth` (`extern "c" fn wcwidth(c: u32) c_int`).
#[cfg(unix)]
#[allow(unsafe_code)]
mod libc_wcwidth {
unsafe extern "C" {
fn wcwidth(c: u32) -> i32;
}
#[allow(unsafe_code)]
#[inline]
pub fn call(cp: u32) -> i32 {
// SAFETY: `wcwidth` is a pure libc function taking a plain integer
// (wchar_t) and returning an int; no pointers or preconditions.
unsafe { wcwidth(cp) }
}
}
/// The `table` mode: generated two-stage lookup (`unicode.table.get(cp).width`),
/// with the same `<= 0xFF` shortcut as terminal.zig.
#[inline]
fn width_table(
table: &ghostty_unicode::lut::Tables<'_, ghostty_unicode::Properties>,
cp: u32,
) -> i32 {
if cp <= 0xFF {
1
} else {
i32::from(table.get(cp).width)
}
}
/// `simd.codepointWidth` (scalar port of the highway implementation).
#[must_use]
pub fn simd_width(cp: u32) -> i32 {
i32::from(crate::simd_width::codepoint_width(cp))
}
impl Benchmark for CodepointWidth {
fn setup(&mut self) -> Result {
self.data.open()
}
fn teardown(&mut self) {
self.data.f = None;
}
fn step(&mut self) -> Result {
match self.mode {
WidthMode::Noop => Ok(()),
WidthMode::Wcwidth => {
#[cfg(unix)]
{
let mut d = Utf8Decoder::new();
self.data.for_each_chunk(|chunk| {
decode_each(&mut d, chunk, |cp| {
black_box(libc_wcwidth::call(cp));
});
Ok(())
})
}
#[cfg(not(unix))]
{
log::warn!("wcwidth is not available on this platform");
Ok(())
}
}
WidthMode::Simd => {
let mut d = Utf8Decoder::new();
self.data.for_each_chunk(|chunk| {
decode_each(&mut d, chunk, |cp| {
black_box(simd_width(cp));
});
Ok(())
})
}
WidthMode::Table => {
let mut d = Utf8Decoder::new();
let table = ghostty_unicode::table();
self.data.for_each_chunk(|chunk| {
decode_each(&mut d, chunk, |cp| {
black_box(width_table(&table, cp));
});
Ok(())
})
}
}
}
}
// ---------------------------------------------------------------------------
// grapheme-break
pub struct GraphemeBreak {
data: Data,
noop: bool,
}
impl GraphemeBreak {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let noop = args.take_enum("mode", false, &[("noop", true), ("table", false)])?;
Ok(GraphemeBreak {
data: Data::new(args.take_opt_path("data")?),
noop,
})
}
}
impl Benchmark for GraphemeBreak {
fn setup(&mut self) -> Result {
self.data.open()
}
fn teardown(&mut self) {
self.data.f = None;
}
fn step(&mut self) -> Result {
let mut d = Utf8Decoder::new();
if self.noop {
return self.data.for_each_chunk(|chunk| {
for &c in chunk {
black_box(d.next(c));
}
Ok(())
});
}
let mut state = ghostty_unicode::BreakState::Default;
let mut cp1 = 0u32;
self.data.for_each_chunk(|chunk| {
decode_each(&mut d, chunk, |cp2| {
black_box(ghostty_unicode::grapheme_break(cp1, cp2, &mut state));
cp1 = cp2;
});
Ok(())
})
}
}
// ---------------------------------------------------------------------------
// is-symbol
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolMode {
/// uucode implementation (computed from general category + block).
Uucode,
/// Ghostty's table-based approach.
Table,
}
pub struct IsSymbol {
data: Data,
mode: SymbolMode,
}
/// Computed `is_symbol` (`computeIsSymbol` in uucode_config.zig): private
/// use (general category Co) or one of the symbol blocks, evaluated directly
/// from the ranges rather than via the generated table.
#[must_use]
pub fn is_symbol_computed(cp: u32) -> bool {
matches!(
cp,
// general_category == other_private_use
0xE000..=0xF8FF | 0xF_0000..=0xF_FFFD | 0x10_0000..=0x10_FFFD
// arrows
| 0x2190..=0x21FF
// dingbats
| 0x2700..=0x27BF
// emoticons
| 0x1F600..=0x1F64F
// miscellaneous_symbols
| 0x2600..=0x26FF
// enclosed_alphanumerics
| 0x2460..=0x24FF
// enclosed_alphanumeric_supplement
| 0x1F100..=0x1F1FF
// miscellaneous_symbols_and_pictographs
| 0x1F300..=0x1F5FF
// transport_and_map_symbols
| 0x1F680..=0x1F6FF
)
}
impl IsSymbol {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let mode = args.take_enum(
"mode",
SymbolMode::Uucode,
&[("uucode", SymbolMode::Uucode), ("table", SymbolMode::Table)],
)?;
Ok(IsSymbol {
data: Data::new(args.take_opt_path("data")?),
mode,
})
}
}
impl Benchmark for IsSymbol {
fn setup(&mut self) -> Result {
self.data.open()
}
fn teardown(&mut self) {
self.data.f = None;
}
fn step(&mut self) -> Result {
let mut d = Utf8Decoder::new();
match self.mode {
SymbolMode::Uucode => self.data.for_each_chunk(|chunk| {
decode_each(&mut d, chunk, |cp| {
black_box(is_symbol_computed(cp));
});
Ok(())
}),
SymbolMode::Table => {
let table = ghostty_unicode::symbols_table::table();
self.data.for_each_chunk(|chunk| {
decode_each(&mut d, chunk, |cp| {
black_box(table.get(cp));
});
Ok(())
})
}
}
}
}
// ---------------------------------------------------------------------------
// screen-clone
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CloneMode {
Noop,
Clone,
Render,
}
pub struct ScreenClone {
data: Data,
mode: CloneMode,
terminal: Terminal,
/// Clones are kept alive so freeing is not benchmarked.
keep: Vec<ghostty_terminal::screen::Screen>,
}
impl ScreenClone {
pub fn create(args: &mut Args) -> std::result::Result<Self, String> {
let mode = args.take_enum(
"mode",
CloneMode::Clone,
&[
("clone", CloneMode::Clone),
("noop", CloneMode::Noop),
("render", CloneMode::Render),
],
)?;
let rows = args.take("terminal-rows", 80u16)?;
let cols = args.take("terminal-cols", 120u16)?;
let data = args.take_opt_path("data")?;
let terminal = Terminal::new(TermOptions::new(cols, rows)).map_err(|e| format!("{e:?}"))?;
Ok(ScreenClone {
data: Data::new(data),
mode,
terminal,
keep: Vec::new(),
})
}
}
impl Benchmark for ScreenClone {
fn setup(&mut self) -> Result {
self.terminal.full_reset();
// Force a style on every single row.
let rows = self.terminal.rows;
let mut s = self.terminal.vt_stream();
let fail = |e: &dyn std::fmt::Debug| warn("error processing setup data", &format!("{e:?}"));
s.next_slice(b"\x1b[48;2;20;40;60m").map_err(|e| fail(&e))?;
for _ in 0..rows - 1 {
s.next_slice(b"hello\r\n").map_err(|e| fail(&e))?;
}
s.next_slice(b"hello").map_err(|e| fail(&e))?;
// Setup our terminal state from the data file (not benchmarked).
self.data.open()?;
self.data.for_each_chunk(|chunk| {
s.next_slice(chunk)
.map_err(|e| warn("error processing data file chunk", &format!("{e:?}")))
})?;
self.data.f = None;
Ok(())
}
fn step(&mut self) -> Result {
// We loop because it's so fast that a single run doesn't capture it.
match self.mode {
CloneMode::Noop => {
for _ in 0..1000 {
black_box(self.terminal.screens.active());
}
}
CloneMode::Clone => {
for _ in 0..1000 {
let s = self.terminal.screens.active();
let copy = s
.clone_screen(Point::Viewport(Coordinate { x: 0, y: 0 }), None)
.map_err(|e| warn("error cloning screen", &format!("{e:?}")))?;
self.keep.push(black_box(copy));
}
}
CloneMode::Render => {
let mut state = RenderState::new();
let fail = |e: &dyn std::fmt::Debug| {
warn("error updating render state", &format!("{e:?}"))
};
// The first run is dominated by allocation; ignore it.
state.update(&mut self.terminal).map_err(|e| fail(&e))?;
for _ in 0..1000 {
// Forces a full rebuild because it thinks our screen changed.
state.screen = ScreenKey::Alternate;
state.update(&mut self.terminal).map_err(|e| fail(&e))?;
black_box(&state);
}
}
}
Ok(())
}
fn teardown(&mut self) {
self.keep.clear();
}
}
// ---------------------------------------------------------------------------
// CLI
/// Run the named action with the given (already `+action`-free) arguments.
/// When `time` is set the measured result is returned.
pub fn main_action(action: &str, mut args: Args) -> std::result::Result<RunResult, String> {
fn go<B: Benchmark>(
b: std::result::Result<B, String>,
args: Args,
) -> std::result::Result<RunResult, String> {
let mut b = b?;
args.finish()?;
run(&mut b, RunMode::Once).map_err(|e| e.to_string())
}
// Options are consumed by `create`; leftovers are errors.
let a = &mut args;
match action {
"screen-clone" => {
let b = ScreenClone::create(a);
go(b, args)
}
"terminal-stream" => {
let b = TerminalStream::create(a);
go(b, args)
}
"codepoint-width" => {
let b = CodepointWidth::create(a);
go(b, args)
}
"grapheme-break" => {
let b = GraphemeBreak::create(a);
go(b, args)
}
"terminal-parser" => {
let b = TerminalParser::create(a);
go(b, args)
}
"is-symbol" => {
let b = IsSymbol::create(a);
go(b, args)
}
"osc-parser" => {
let b = OscParser::create(a);
go(b, args)
}
_ => Err(format!("invalid action: +{action}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(action: &str, mode: &str, data: &std::path::Path) -> Result {
let mut args = Args::parse([
std::ffi::OsString::from(format!("+{action}")),
std::ffi::OsString::from(format!("--mode={mode}")),
{
let mut a = std::ffi::OsString::from("--data=");
a.push(data);
a
},
])
.unwrap();
match action {
"codepoint-width" => crate::benchmark::run(
&mut CodepointWidth::create(&mut args).unwrap(),
crate::benchmark::RunMode::Once,
),
_ => crate::benchmark::run(
&mut IsSymbol::create(&mut args).unwrap(),
crate::benchmark::RunMode::Once,
),
}
.map(|_| ())
}
fn sample_file(tag: &str) -> std::path::PathBuf {
let p =
std::env::temp_dir().join(format!("ghostty-bench-{tag}-{}.txt", std::process::id()));
std::fs::write(
&p,
"hello \u{4e00}\u{1f600}\u{301}\u{e000} world \u{2190}\n",
)
.unwrap();
p
}
#[test]
fn codepoint_width_all_modes_run() {
let p = sample_file("cw");
for m in ["noop", "wcwidth", "simd", "table"] {
run("codepoint-width", m, &p).unwrap();
}
let mut args = Args::parse(["--mode=bogus".to_owned()]).unwrap();
assert!(CodepointWidth::create(&mut args).is_err());
let _ = std::fs::remove_file(p);
}
#[test]
fn duplicate_bad_enum_modes_fail() {
let mk = |v: &[&str]| Args::parse(v.iter().map(|s| (*s).to_owned())).unwrap();
for v in [
&["--mode=bad", "--mode=noop"][..],
&["--mode=bad", "--mode="][..],
] {
assert!(CodepointWidth::create(&mut mk(v)).is_err());
assert!(GraphemeBreak::create(&mut mk(v)).is_err());
assert!(IsSymbol::create(&mut mk(v)).is_err());
assert!(ScreenClone::create(&mut mk(v)).is_err());
}
}
#[test]
fn is_symbol_modes_run() {
let p = sample_file("sym");
for m in ["uucode", "table"] {
run("is-symbol", m, &p).unwrap();
}
let _ = std::fs::remove_file(p);
}
#[test]
fn is_symbol_computed_matches_table() {
let t = ghostty_unicode::symbols_table::table();
for cp in 0..=0x11_0000u32 {
assert_eq!(is_symbol_computed(cp), t.get(cp), "cp={cp:x}");
}
}
#[test]
fn simd_matches_table_on_samples() {
let t = ghostty_unicode::table();
for cp in [
0x61, 0x100, 0x3400, 0x2E3A, 0x1F1E6, 0x4E00, 0xF900, 0x1F600, 0x20000, 0x30000, 0x301,
0x200B, 0x200D, 0xFE0F, 0x20DD, 0xE0100, 0xE0001, 0x1160, 0xAC00,
] {
assert_eq!(simd_width(cp), width_table(&t, cp), "cp={cp:x}");
}
}
#[cfg(unix)]
#[test]
fn wcwidth_ascii() {
// The C locale is in effect (no setlocale, like Zig): ASCII only.
assert_eq!(libc_wcwidth::call(u32::from(b'a')), 1);
assert_eq!(libc_wcwidth::call(0), 0);
}
}