perf(terminal): make screen clone match Zig, batch OSC payload copies

screen-clone was 2.6x slower than Zig. The main cost was in
PageList::clone_range: it created a throwaway page, recycled it into the
pool and Page::reinit then memset the entire page again, faulting in
every cell. Untouched pages now go to a clean_pool that create_node
reuses without re-zeroing. Page::init allocates cells with
bytemuck::allocation::try_zeroed_vec (Cell derives Zeroable), getting
lazily-zeroed memory from the allocator without unsafe code, and
clone_partial_row_from bulk-copies plain-cell runs in rows with managed
memory. screen-clone clone: 78 ms -> ~28 ms (Zig ~27.8 ms).

osc::Parser::next_slice appends payload bytes in bulk while behaving
exactly like per-byte next() (2048 limit, overflow), verified by a
property test; the osc-parser bench uses it (54.5 -> ~32 ms, Zig ~28).

Still slower: screen-clone render (~2.4x), dominated by per-row style
vectors in RenderState::update.

Test Plan:
- cargo test --workspace (no failures)
- ghostty-bench +screen-clone / +osc-parser with --profile production
This commit is contained in:
ddidderr committed 2026-10-12 03:51:46 +02:00
1 parent bd48c39d5f
commit e423dd5586
7 files changed
+107 -17

No files matched your search

+12 -2
View File
@@ -153,6 +153,8 @@ pub struct PageList {
nodes: Vec<Option<Node>>,
free_nodes: Vec<usize>,
page_pool: Vec<Page>,
/// Never-written pages (all cells zero) that need no `reinit` memset.
clean_pool: Vec<Page>,
/// Oldest page.
pub first: Option<NodeId>,
/// Last active page.
@@ -211,6 +213,7 @@ impl PageList {
nodes: Vec::new(),
free_nodes: Vec::new(),
page_pool: Vec::new(),
clean_pool: Vec::new(),
first: None,
last: None,
page_serial: 0,
@@ -257,7 +260,13 @@ impl PageList {
return Err(Error::OutOfSpace);
}
self.nodes.try_reserve(1).map_err(|_| Error::OutOfMemory)?;
let mut data = if let Some(pos) = self.page_pool.iter().position(|p| p.capacity == cap) {
let mut data = if let Some(mut p) = self.clean_pool.pop().filter(|p| p.capacity == cap) {
p.size = page::Size {
cols: cap.cols,
rows: cap.rows,
};
p
} else if let Some(pos) = self.page_pool.iter().position(|p| p.capacity == cap) {
let mut p = self.page_pool.swap_remove(pos);
p.reinit()?;
p
@@ -2737,7 +2746,8 @@ impl PageList {
let mut result = Self::init(self.cols, self.rows, Some(self.explicit_max_size))?;
while let Some(id) = result.first {
let node = result.detach(id).ok_or(Error::InvalidId)?;
result.recycle(node);
// Freshly initialised and never written: reuse without re-zeroing.
result.clean_pool.push(node.data);
}
result.page_serial = 0;
result.total_rows = 0;