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
8048ee6a05
commit
0c4f0ee412
18 files changed
+2195
-82
No files matched your search
@@ -613,6 +613,12 @@ impl Terminal {
|
||||
// We have a printable character, save it
|
||||
self.previous_char = Some(c);
|
||||
|
||||
// Fast path: the overwhelmingly common case of printing a single-width
|
||||
// character in the middle of a line onto a plain narrow cell.
|
||||
if width == 1 && !self.modes.get(Mode::Insert) && self.print_fast(c, right_limit) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If we're soft-wrapping, then handle that first.
|
||||
if self.screen().cursor.pending_wrap && self.modes.get(Mode::Wraparound) {
|
||||
self.print_wrap()?;
|
||||
@@ -693,6 +699,66 @@ impl Terminal {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fast path of `print` for a single-width character that does not hit
|
||||
/// the cursor's right limit. Performs exactly what the general path does
|
||||
/// (mark dirty, write the cell, move right) with a single page lookup.
|
||||
/// Returns `false` without touching anything if any precondition for the
|
||||
/// shortcut does not hold; the caller then takes the general path.
|
||||
#[inline]
|
||||
fn print_fast(&mut self, c: u32, right_limit: size::CellCountInt) -> bool {
|
||||
let scr = self.screens.active_mut();
|
||||
if scr.cursor.pending_wrap
|
||||
|| scr.cursor.hyperlink_id != 0
|
||||
|| scr.charset.single_shift.is_some()
|
||||
|| c == KITTY_PLACEHOLDER
|
||||
|| usize::from(scr.cursor.x) + 1 >= usize::from(right_limit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let set = scr.charset.charsets.get(scr.charset.gl);
|
||||
if set != Charset::Utf8 && set != Charset::Ascii {
|
||||
return false;
|
||||
}
|
||||
|
||||
let style_id = scr.cursor.style_id;
|
||||
let protected = scr.cursor.protected;
|
||||
let semantic = scr.cursor.semantic_content;
|
||||
let pin_handle = scr.cursor.page_pin;
|
||||
let Some(pin) = scr.pages.tracked_pin(pin_handle) else {
|
||||
return false;
|
||||
};
|
||||
let (x, y) = (usize::from(pin.x), usize::from(pin.y));
|
||||
let page = scr.page_mut(pin.node);
|
||||
let Ok(idx) = page.cell_index(x, y) else {
|
||||
return false;
|
||||
};
|
||||
let old = page.cells[idx];
|
||||
if old.wide() != Wide::Narrow
|
||||
|| old.has_grapheme()
|
||||
|| old.hyperlink()
|
||||
|| old.style_id() != style_id
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same cell value as `print_cell` produces.
|
||||
let mut new_cell = Cell::init(c);
|
||||
new_cell.set_content_tag(ContentTag::Codepoint);
|
||||
new_cell.set_style_id(style_id);
|
||||
new_cell.set_wide(Wide::Narrow);
|
||||
new_cell.set_protected(protected);
|
||||
new_cell.set_semantic_content(semantic);
|
||||
page.cells[idx] = new_cell;
|
||||
page.rows[y].set_dirty(true);
|
||||
|
||||
// Move the cursor right (`Screen::cursor_right(1)`).
|
||||
if let Some(p) = scr.pages.tracked_pin_x_mut(pin_handle) {
|
||||
p.x += 1;
|
||||
}
|
||||
scr.cursor.x += 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// The grapheme-clustering section of `print`. Returns `true` if the
|
||||
/// character was fully handled (Zig: `return`), `false` if printing should
|
||||
/// continue on the normal path (Zig: `break :grapheme`).
|
||||
|
||||
Reference in new issue
Block a user