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
97 lines
3.2 KiB
Rust
97 lines
3.2 KiB
Rust
//! 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)
|
|
}
|