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:
1 parent
e2bd4c28da
commit
e9fdce0b26
7 files changed
+107
-17
No files matched your search
@@ -5,6 +5,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bytemuck = { version = "1.25.2", features = ["derive", "extern_crate_alloc"] }
|
||||
crossbeam-channel = "0.5.17"
|
||||
flate2 = "1.1.10"
|
||||
ghostty-datastruct.workspace = true
|
||||
|
||||
@@ -76,7 +76,7 @@ pub enum SemanticPrompt {
|
||||
PromptContinuation = 2,
|
||||
}
|
||||
/// Eight-byte terminal cell; all-zero bits describe an empty cell.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, bytemuck::Zeroable)]
|
||||
#[repr(transparent)]
|
||||
pub struct Cell(u64);
|
||||
impl Cell {
|
||||
@@ -744,12 +744,10 @@ impl Page {
|
||||
row.set_cells(usize::from(y) * usize::from(cap.cols));
|
||||
rows.push(row);
|
||||
}
|
||||
let mut cells = Vec::new();
|
||||
let count = usize::from(cap.cols) * usize::from(cap.rows);
|
||||
cells
|
||||
.try_reserve_exact(count)
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
cells.resize(count, Cell::default());
|
||||
// Zeroed allocation: large buffers come lazily zeroed from the OS.
|
||||
let cells: Vec<Cell> =
|
||||
bytemuck::allocation::try_zeroed_vec(count).map_err(|()| Error::OutOfMemory)?;
|
||||
let grapheme_alloc = ChunkAlloc::new(l.grapheme_alloc_layout, 16)?;
|
||||
let mut grapheme_data = Vec::new();
|
||||
let codepoints = grapheme_alloc.chunks * 4;
|
||||
@@ -1486,15 +1484,18 @@ impl Page {
|
||||
// Row metadata/cell indices were validated above (`get_row`).
|
||||
let from = source_row.cells() + src_x;
|
||||
let to = original_row.cells() + dst_x;
|
||||
for n in 0..len {
|
||||
let cell = src.cells[from + n];
|
||||
let src_cells = &src.cells[from..from + len];
|
||||
let mut run = 0;
|
||||
for (n, cell) in src_cells.iter().enumerate() {
|
||||
if !cell.has_grapheme() && !cell.hyperlink() && !cell.has_styling() {
|
||||
// Plain cell: no managed memory to copy.
|
||||
self.cells[to + n] = cell;
|
||||
} else {
|
||||
self.clone_cell_from_cleared(src, from + n, dst_x + n, dst_y)?;
|
||||
continue;
|
||||
}
|
||||
// Bulk-copy the preceding run of plain cells (no managed memory).
|
||||
self.cells[to + run..to + n].copy_from_slice(&src_cells[run..n]);
|
||||
self.clone_cell_from_cleared(src, from + n, dst_x + n, dst_y)?;
|
||||
run = n + 1;
|
||||
}
|
||||
self.cells[to + run..to + len].copy_from_slice(&src_cells[run..len]);
|
||||
} else {
|
||||
let from = source_row.cells() + src_x;
|
||||
let to = original_row.cells() + dst_x;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user