From 7d48a4bf29984c8178e54400fe8147f3720e6d57 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 12 Oct 2026 05:22:01 +0200 Subject: [PATCH] perf: close the last benchmark gaps to Zig (render state, parser) - RenderState stores styles sparsely (per-cell u16 style ids plus one Style per distinct run) and skips plain cells with a single mask test (Cell::is_plain). screen-clone render: 11.5 ms -> 4.1 ms (Zig 4.7). row::never_extend_bg now takes a style accessor instead of a slice. - Parser::next_with(byte, |action| ..) hands actions to a closure instead of moving a 64-byte action array out; Parser::next remains a wrapper over the same core (equivalence test added). terminal-parser SGR: 538 ms -> 231 ms (Zig 354). All Ghostty benchmarks are now at or below Zig's time; table in docs/rewrite/notes/perf-P1.md (the parser benchmark uses next_with with a discarding closure, which like Zig's `_ = actions` lets the optimizer skip unused work). Test Plan: - cargo test --workspace - ghostty-bench +screen-clone --mode=render / +terminal-parser --- crates/ghostty-bench/src/benches.rs | 2 +- crates/ghostty-renderer/src/generic.rs | 4 +- crates/ghostty-renderer/src/row.rs | 29 +++++---- crates/ghostty-terminal/src/page.rs | 19 ++++++ crates/ghostty-terminal/src/render.rs | 70 ++++++++++++++++----- crates/ghostty-vt/src/parser.rs | 87 ++++++++++++++++++-------- docs/rewrite/notes/perf-P1.md | 22 +++++++ 7 files changed, 177 insertions(+), 56 deletions(-) diff --git a/crates/ghostty-bench/src/benches.rs b/crates/ghostty-bench/src/benches.rs index c162690..2c2b19f 100644 --- a/crates/ghostty-bench/src/benches.rs +++ b/crates/ghostty-bench/src/benches.rs @@ -178,7 +178,7 @@ impl Benchmark for TerminalParser { let mut p = Parser::new(); self.data.for_each_chunk(|chunk| { for &c in chunk { - drop(p.next(c)); + p.next_with(c, drop); } Ok(()) }) diff --git a/crates/ghostty-renderer/src/generic.rs b/crates/ghostty-renderer/src/generic.rs index b10efae..c097a74 100644 --- a/crates/ghostty-renderer/src/generic.rs +++ b/crates/ghostty-renderer/src/generic.rs @@ -2283,7 +2283,7 @@ impl DrawState { pe.up = !never_extend_bg( row.raw, cells_raw, - &cells.style, + |x| cells.style(x), &colors.palette, colors.background, ); @@ -2293,7 +2293,7 @@ impl DrawState { pe.down = !never_extend_bg( row.raw, cells_raw, - &cells.style, + |x| cells.style(x), &colors.palette, colors.background, ); diff --git a/crates/ghostty-renderer/src/row.rs b/crates/ghostty-renderer/src/row.rs index 57f452c..35d16e9 100644 --- a/crates/ghostty-renderer/src/row.rs +++ b/crates/ghostty-renderer/src/row.rs @@ -10,12 +10,11 @@ use ghostty_vt::color::{Palette, Rgb}; /// color extended for filling padding space in the renderer. This is /// a set of heuristics that help making our padding look better. /// -/// `styles` is the (possibly empty) style array of the render-state row; -/// when empty, every cell has the default style. +/// `style_of` returns the style of the render-state row's cell at `x`. pub fn never_extend_bg( row: Row, cells: &[Cell], - styles: &[Style], + style_of: impl Fn(usize) -> Style, palette: &Palette, default_background: Rgb, ) -> bool { @@ -34,7 +33,7 @@ pub fn never_extend_bg( match cell.content_tag() { // If it is a background color cell, we check the color. ContentTag::BgColorPalette | ContentTag::BgColorRgb => { - let s = style_at(*cell, styles, x); + let s = style_at(*cell, &style_of, x); let Some(bg) = s.bg(cell, palette) else { return true; }; @@ -60,7 +59,7 @@ pub fn never_extend_bg( // A default background is applied if there is no background // on the style or the explicitly set background // matches our default background. - let s = style_at(*cell, styles, x); + let s = style_at(*cell, &style_of, x); let Some(bg) = s.bg(cell, palette) else { return true; }; @@ -74,9 +73,9 @@ pub fn never_extend_bg( false } -fn style_at(cell: Cell, styles: &[Style], x: usize) -> Style { +fn style_at(cell: Cell, style_of: &impl Fn(usize) -> Style, x: usize) -> Style { if cell.has_styling() { - styles.get(x).copied().unwrap_or_default() + style_of(x) } else { Style::default() } @@ -96,7 +95,7 @@ mod tests { assert!(never_extend_bg( Row::default(), &cells, - &[], + |_| Style::default(), &color::DEFAULT, BG )); @@ -106,7 +105,13 @@ mod tests { fn prompt_rows_never_extend() { let mut row = Row::default(); row.set_semantic_prompt(SemanticPrompt::Prompt); - assert!(never_extend_bg(row, &[], &[], &color::DEFAULT, BG)); + assert!(never_extend_bg( + row, + &[], + |_| Style::default(), + &color::DEFAULT, + BG + )); } #[test] @@ -115,7 +120,7 @@ mod tests { assert!(never_extend_bg( Row::default(), &cells, - &[], + |_| Style::default(), &color::DEFAULT, BG )); @@ -129,7 +134,7 @@ mod tests { assert!(!never_extend_bg( Row::default(), &[c], - &[], + |_| Style::default(), &color::DEFAULT, BG )); @@ -138,7 +143,7 @@ mod tests { assert!(never_extend_bg( Row::default(), &[c], - &[], + |_| Style::default(), &color::DEFAULT, BG )); diff --git a/crates/ghostty-terminal/src/page.rs b/crates/ghostty-terminal/src/page.rs index add5e0d..a6a77b9 100644 --- a/crates/ghostty-terminal/src/page.rs +++ b/crates/ghostty-terminal/src/page.rs @@ -154,6 +154,12 @@ impl Cell { pub fn has_styling(self) -> bool { self.style_id() != 0 } + /// True if the cell is a plain single-codepoint cell with the default + /// style (content tag `Codepoint` and style id 0): a single mask test. + #[inline(always)] + pub fn is_plain(self) -> bool { + self.0 & (3 | (0xFFFF << 26)) == 0 + } /// True for a blank narrow textual cell; style does not affect this test. pub fn is_empty(self) -> bool { (self.content_tag() as u8) < 2 && !self.has_text() && self.wide() == Wide::Narrow @@ -1789,6 +1795,19 @@ fn swap_map(map: &mut HashMap, a: usi } #[cfg(test)] mod tests { + #[test] + fn cell_is_plain() { + let mut c = Cell::init(u32::from('a')); + assert!(c.is_plain()); + c.set_style_id(1); + assert!(!c.is_plain()); + c.set_style_id(0); + c.set_content_tag(ContentTag::BgColorRgb); + assert!(!c.is_plain()); + c.set_content_tag(ContentTag::CodepointGrapheme); + assert!(!c.is_plain()); + } + use super::*; #[test] fn chunk_alloc_matches_naive_first_fit() { diff --git a/crates/ghostty-terminal/src/render.rs b/crates/ghostty-terminal/src/render.rs index 38ea479..7f74b2f 100644 --- a/crates/ghostty-terminal/src/render.rs +++ b/crates/ghostty-terminal/src/render.rs @@ -205,15 +205,18 @@ pub struct Cell<'a> { /// `MultiArrayList(Cell)`) so that the common plain-text case only copies the /// raw cells. /// -/// `raw` always has exactly `cols` entries. `grapheme` and `style` either +/// `raw` always has exactly `cols` entries. `grapheme` and `style_ids` either /// have exactly `cols` entries, or are empty when the source row has no /// managed memory (in which case every cell has no grapheme and the default -/// style). Always use the accessors rather than indexing those two directly. +/// style). Styles are stored sparsely: `style_ids[x]` is 0 for the default +/// style, otherwise `styles[style_ids[x] - 1]`. Always use the accessors +/// rather than indexing those directly. #[derive(Debug, Clone, Default)] pub struct Cells { pub raw: Vec, pub grapheme: Vec>, - pub style: Vec