feat: cache the symbol width in the cell

This leads to more than a 50% speedup
This commit is contained in:
Josh McKinney 2024-08-25 01:14:48 -07:00
parent 2b391ac15d
commit f08640c53b
No known key found for this signature in database
GPG key ID: 722287396A903BC5
3 changed files with 82 additions and 13 deletions

View file

@ -6,8 +6,6 @@ use std::{
io, iter,
};
use unicode_width::UnicodeWidthStr;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::{Buffer, Cell},
@ -52,13 +50,13 @@ fn buffer_view(buffer: &Buffer) -> String {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
for (x, cell) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(c.symbol());
view.push_str(cell.symbol());
} else {
overwritten.push((x, c.symbol()));
overwritten.push((x, cell.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
skip = std::cmp::max(skip, cell.width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {

View file

@ -499,10 +499,8 @@ impl Buffer {
updates.push((x, y, &next_buffer[i]));
}
let current_symbol_width = current.symbol().width();
let previous_symbol_width = previous.symbol().width();
to_skip = current_symbol_width.saturating_sub(1);
let affected_width = std::cmp::max(current_symbol_width, previous_symbol_width);
to_skip = current.width().saturating_sub(1);
let affected_width = std::cmp::max(current.width(), previous.width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
@ -599,7 +597,7 @@ impl fmt::Debug for Buffer {
} else {
overwritten.push((x, c.symbol()));
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
skip = std::cmp::max(skip, c.width()).saturating_sub(1);
#[cfg(feature = "underline-color")]
{
let style = (c.fg, c.bg, c.underline_color, c.modifier);

View file

@ -1,9 +1,12 @@
use std::hash::{Hash, Hasher};
use compact_str::CompactString;
use unicode_width::UnicodeWidthStr;
use crate::style::{Color, Modifier, Style};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cell {
/// The string to be drawn in the cell.
@ -31,11 +34,53 @@ pub struct Cell {
/// Whether the cell should be skipped when copying (diffing) the buffer to the screen.
pub skip: bool,
/// Cache the width of the cell.
width: std::cell::Cell<Option<usize>>,
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
self.symbol == other.symbol
&& self.fg == other.fg
&& self.bg == other.bg
&& self.underline_color == other.underline_color
&& self.modifier == other.modifier
&& self.skip == other.skip
// explicitly not comparing width, as it is a cache and may be not set
// && self.width == other.width
}
}
impl Hash for Cell {
fn hash<H: Hasher>(&self, state: &mut H) {
self.symbol.hash(state);
self.fg.hash(state);
self.bg.hash(state);
self.underline_color.hash(state);
self.modifier.hash(state);
self.skip.hash(state);
// explicitly not hashing width, as it is a cache and not part of the cell's identity
// self.width.hash(state);
}
}
impl Cell {
/// An empty `Cell`
pub const EMPTY: Self = Self::new(" ");
pub const EMPTY: Self = {
Self {
symbol: CompactString::const_new(" "),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(Some(1)),
}
};
/// Creates a new `Cell` with the given symbol.
///
@ -52,6 +97,7 @@ impl Cell {
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(None),
}
}
@ -64,6 +110,7 @@ impl Cell {
/// Sets the symbol of the cell.
pub fn set_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol = CompactString::new(symbol);
self.width.set(None);
self
}
@ -72,6 +119,7 @@ impl Cell {
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self.width.set(None);
self
}
@ -79,6 +127,7 @@ impl Cell {
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
self.symbol = CompactString::new(ch.encode_utf8(&mut buf));
self.width.set(None);
self
}
@ -148,6 +197,21 @@ impl Cell {
}
self.modifier = Modifier::empty();
self.skip = false;
self.width.set(Some(1));
}
/// Returns the width of the cell.
///
/// This value is cached and will only be recomputed when the cell is modified.
#[must_use]
pub fn width(&self) -> usize {
if let Some(width) = self.width.get() {
width
} else {
let width = self.symbol().width();
self.width.set(Some(width));
width
}
}
}
@ -182,6 +246,7 @@ mod tests {
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
width: std::cell::Cell::new(None),
}
);
}
@ -294,4 +359,12 @@ mod tests {
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
}
#[test]
fn width() {
let cell = Cell::new("");
assert_eq!(cell.width, std::cell::Cell::new(None)); // not yet cached
assert_eq!(cell.width(), 2);
assert_eq!(cell.width, std::cell::Cell::new(Some(2))); // cached
}
}