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
@@ -496,6 +496,37 @@ impl Parser {
|
||||
self.next_state(c);
|
||||
}
|
||||
|
||||
/// Consume a run of characters; exactly equivalent to calling
|
||||
/// [`Parser::next`] for each byte, but payload bytes are appended in bulk.
|
||||
pub fn next_slice(&mut self, mut bytes: &[u8]) {
|
||||
while let Some((&c, rest)) = bytes.split_first() {
|
||||
if self.state == State::Invalid {
|
||||
return;
|
||||
}
|
||||
match self.writer {
|
||||
Writer::None => {
|
||||
self.next_state(c);
|
||||
bytes = rest;
|
||||
}
|
||||
Writer::Allocating => {
|
||||
self.buffer.extend_from_slice(bytes);
|
||||
return;
|
||||
}
|
||||
Writer::Fixed => {
|
||||
let room = Self::MAX_BUF.saturating_sub(self.buffer.len());
|
||||
if bytes.len() <= room {
|
||||
self.buffer.extend_from_slice(bytes);
|
||||
} else {
|
||||
// Overflow: keep what fits, discard the rest.
|
||||
self.buffer.extend_from_slice(&bytes[..room]);
|
||||
self.state = State::Invalid;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn next_state(&mut self, c: u8) {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
@@ -749,3 +780,31 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod next_slice_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_slice_matches_next() {
|
||||
for alloc in [false, true] {
|
||||
for prefix in ["0;", "52;c;", "4;", "7;", "8;;", "99;", "x"] {
|
||||
for n in [0usize, 1, 5, 2040, 2046, 2047, 2048, 2049, 2050, 3000] {
|
||||
let mut input = prefix.as_bytes().to_vec();
|
||||
input.extend((0..n).map(|i| b'a' + (i % 26) as u8));
|
||||
let mut a = Parser::init(alloc);
|
||||
let mut b = Parser::init(alloc);
|
||||
for &c in &input {
|
||||
a.next(c);
|
||||
}
|
||||
let mid = input.len() / 2;
|
||||
b.next_slice(&input[..mid]);
|
||||
b.next_slice(&input[mid..]);
|
||||
assert_eq!(a.state, b.state);
|
||||
assert_eq!(a.buffer, b.buffer);
|
||||
assert_eq!(a.writer, b.writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user