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:
1 parent
c6a0c49567
commit
062c643ea8
18 files changed
+2195
-82
No files matched your search
@@ -0,0 +1,96 @@
|
||||
//! Minimal `--key=value` option parsing (the subset of `cli/args.zig` used by
|
||||
//! the benchmark and synthetic CLIs).
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Parsed `--key[=value]` arguments.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Args {
|
||||
map: HashMap<String, Option<String>>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Parse arguments (skipping `+action` words and the program name).
|
||||
pub fn parse<I: IntoIterator<Item = String>>(iter: I) -> Result<Args, String> {
|
||||
let mut map = HashMap::new();
|
||||
for arg in iter {
|
||||
if arg.starts_with('+') {
|
||||
continue;
|
||||
}
|
||||
let Some(rest) = arg.strip_prefix("--") else {
|
||||
return Err(format!("unexpected argument: {arg}"));
|
||||
};
|
||||
match rest.split_once('=') {
|
||||
Some((k, v)) => map.insert(k.to_owned(), Some(v.to_owned())),
|
||||
None => map.insert(rest.to_owned(), None),
|
||||
};
|
||||
}
|
||||
Ok(Args { map })
|
||||
}
|
||||
|
||||
/// Take a typed value, falling back to `default`.
|
||||
pub fn take<T: FromStr>(&mut self, key: &str, default: T) -> Result<T, String> {
|
||||
match self.map.remove(key) {
|
||||
None => Ok(default),
|
||||
Some(None) => Err(format!("option --{key} requires a value")),
|
||||
Some(Some(v)) => v
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid value for --{key}: {v}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_opt_string(&mut self, key: &str) -> Result<Option<String>, String> {
|
||||
match self.map.remove(key) {
|
||||
None => Ok(None),
|
||||
Some(None) => Err(format!("option --{key} requires a value")),
|
||||
Some(Some(v)) => Ok(Some(v)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a boolean flag (`--key`, `--key=true`, `--key=false`).
|
||||
pub fn take_flag(&mut self, key: &str) -> Result<bool, String> {
|
||||
match self.map.remove(key) {
|
||||
None => Ok(false),
|
||||
Some(None) => Ok(true),
|
||||
Some(Some(v)) => match v.as_str() {
|
||||
"true" | "t" | "1" | "yes" => Ok(true),
|
||||
"false" | "f" | "0" | "no" => Ok(false),
|
||||
_ => Err(format!("invalid value for --{key}: {v}")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Error on any unconsumed option.
|
||||
pub fn finish(self) -> Result<(), String> {
|
||||
match self.map.into_keys().next() {
|
||||
Some(k) => Err(format!("unknown option --{k}")),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the data source for the path: `None` if unset, stdin for `-`,
|
||||
/// otherwise the opened file (mirrors `benchmark/options.zig`).
|
||||
pub fn data_file(path: Option<&str>) -> io::Result<Option<Box<dyn Read>>> {
|
||||
match path {
|
||||
None => Ok(None),
|
||||
Some("-") => Ok(Some(Box::new(io::stdin()))),
|
||||
Some(p) => Ok(Some(Box::new(File::open(p)?))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read until `buf` is full or EOF (Zig `readSliceShort`).
|
||||
pub fn read_short(r: &mut dyn Read, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut n = 0;
|
||||
while n < buf.len() {
|
||||
match r.read(&mut buf[n..]) {
|
||||
Ok(0) => break,
|
||||
Ok(m) => n += m,
|
||||
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
Reference in new issue
Block a user