1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-24 05:42:03 +03:00

micro-optimize clustering

This improves it by ~4x for long lines, taking it from 120us to ~30us.
This commit is contained in:
Wez Furlong 2021-04-28 08:19:44 -07:00
parent 8953bff276
commit f91ca30008
5 changed files with 129 additions and 19 deletions

View File

@ -539,7 +539,12 @@ impl Cell {
/// Returns the number of cells visually occupied by this grapheme
pub fn width(&self) -> usize {
grapheme_column_width(self.str())
let s = self.str();
if s.len() == 1 {
1
} else {
grapheme_column_width(s)
}
}
/// Returns the attributes of the cell

View File

@ -1,4 +1,5 @@
use crate::cell::{Cell, CellAttributes};
use std::borrow::Cow;
/// A `CellCluster` is another representation of a Line.
/// A `Vec<CellCluster>` is produced by walking through the Cells in
@ -9,34 +10,97 @@ use crate::cell::{Cell, CellAttributes};
pub struct CellCluster {
pub attrs: CellAttributes,
pub text: String,
pub byte_to_cell_idx: Vec<usize>,
byte_to_cell_idx: Vec<usize>,
first_cell_idx: usize,
}
impl CellCluster {
/// Given a byte index into `self.text`, return the corresponding
/// cell index in the originating line.
pub fn byte_to_cell_idx(&self, byte_idx: usize) -> usize {
if self.byte_to_cell_idx.is_empty() {
self.first_cell_idx + byte_idx
} else {
self.byte_to_cell_idx[byte_idx]
}
}
/// Compute the list of CellClusters from a set of visible cells.
/// The input is typically the result of calling `Line::visible_cells()`.
pub fn make_cluster<'a>(iter: impl Iterator<Item = (usize, &'a Cell)>) -> Vec<CellCluster> {
pub fn make_cluster<'a>(
hint: usize,
iter: impl Iterator<Item = (usize, &'a Cell)>,
) -> Vec<CellCluster> {
let mut last_cluster = None;
let mut clusters = Vec::new();
let mut whitespace_run = 0;
let mut only_whitespace = false;
for (cell_idx, c) in iter {
let cell_str = c.str();
let normalized_attr = c.attrs().clone().set_wrapped(false).clone();
let normalized_attr = if c.attrs().wrapped() {
let mut attr_storage = c.attrs().clone();
attr_storage.set_wrapped(false);
Cow::Owned(attr_storage)
} else {
Cow::Borrowed(c.attrs())
};
last_cluster = match last_cluster.take() {
None => {
// Start new cluster
Some(CellCluster::new(c.attrs().clone(), cell_str, cell_idx))
only_whitespace = cell_str == " ";
whitespace_run = if only_whitespace { 1 } else { 0 };
Some(CellCluster::new(
hint,
normalized_attr.into_owned(),
cell_str,
cell_idx,
))
}
Some(mut last) => {
if last.attrs != normalized_attr {
if last.attrs != *normalized_attr {
// Flush pending cluster and start a new one
clusters.push(last);
Some(CellCluster::new(normalized_attr, cell_str, cell_idx))
only_whitespace = cell_str == " ";
whitespace_run = if only_whitespace { 1 } else { 0 };
Some(CellCluster::new(
hint,
normalized_attr.into_owned(),
cell_str,
cell_idx,
))
} else {
// Add to current cluster
last.add(cell_str, cell_idx);
Some(last)
// Add to current cluster.
// Force cluster to break when we get a run of 2 whitespace
// characters following non-whitespace.
// This reduces the amount of shaping work for scenarios where
// the terminal is wide and a long series of short lines are printed;
// the shaper can cache the few variations of trailing whitespace
// and focus on shaping the shorter cluster sequences.
if cell_str == " " {
whitespace_run += 1;
} else {
whitespace_run = 0;
only_whitespace = false;
}
if !only_whitespace && whitespace_run > 2 {
clusters.push(last);
only_whitespace = cell_str == " ";
whitespace_run = 1;
Some(CellCluster::new(
hint,
normalized_attr.into_owned(),
cell_str,
cell_idx,
))
} else {
last.add(cell_str, cell_idx);
Some(last)
}
}
}
};
@ -51,22 +115,43 @@ impl CellCluster {
}
/// Start off a new cluster with some initial data
fn new(attrs: CellAttributes, text: &str, cell_idx: usize) -> CellCluster {
fn new(hint: usize, attrs: CellAttributes, text: &str, cell_idx: usize) -> CellCluster {
let mut idx = Vec::new();
for _ in 0..text.len() {
idx.push(cell_idx);
if text.len() > 1 {
// Prefer to avoid pushing any index data; this saves
// allocating any storage until we have any cells that
// are multibyte
for _ in 0..text.len() {
idx.push(cell_idx);
}
}
let mut storage = String::with_capacity(hint);
storage.push_str(text);
CellCluster {
attrs,
text: text.into(),
text: storage,
byte_to_cell_idx: idx,
first_cell_idx: cell_idx,
}
}
/// Add to this cluster
fn add(&mut self, text: &str, cell_idx: usize) {
for _ in 0..text.len() {
self.byte_to_cell_idx.push(cell_idx);
if !self.byte_to_cell_idx.is_empty() {
// We had at least one multi-byte cell in the past
for _ in 0..text.len() {
self.byte_to_cell_idx.push(cell_idx);
}
} else if text.len() > 1 {
// Extrapolate the indices so far
for n in 0..self.text.len() {
self.byte_to_cell_idx.push(n + self.first_cell_idx);
}
// Now add this new multi-byte cell text
for _ in 0..text.len() {
self.byte_to_cell_idx.push(cell_idx);
}
}
self.text.push_str(text);
}

View File

@ -432,7 +432,7 @@ impl Line {
}
pub fn cluster(&self) -> Vec<CellCluster> {
CellCluster::make_cluster(self.visible_cells())
CellCluster::make_cluster(self.cells.len(), self.visible_cells())
}
pub fn cells(&self) -> &[Cell] {

View File

@ -365,6 +365,7 @@ impl FontShaper for HarfbuzzShaper {
dpi: u32,
no_glyphs: &mut Vec<char>,
) -> anyhow::Result<Vec<GlyphInfo>> {
log::trace!("shape {} `{}`", text.len(), text);
let start = std::time::Instant::now();
let result = self.do_shape(0, text, size, dpi, no_glyphs);
metrics::histogram!("shape.harfbuzz", start.elapsed());

View File

@ -720,7 +720,13 @@ impl super::TermWindow {
}
// Break the line into clusters of cells with the same attributes
let start = Instant::now();
let cell_clusters = params.line.cluster();
log::trace!(
"cluster -> {} clusters, elapsed {:?}",
cell_clusters.len(),
start.elapsed()
);
let mut last_cell_idx = 0;
@ -811,6 +817,8 @@ impl super::TermWindow {
);
// Shape the printable text from this cluster
let shape_resolve_start = Instant::now();
let glyph_info = {
let key = BorrowedShapeCacheKey {
style,
@ -858,9 +866,14 @@ impl super::TermWindow {
}
}
};
log::trace!(
"shape_resolve for cluster len {} -> elapsed {:?}",
cluster.text.len(),
shape_resolve_start.elapsed()
);
for info in glyph_info.iter() {
let cell_idx = cluster.byte_to_cell_idx[info.pos.cluster as usize];
let cell_idx = cluster.byte_to_cell_idx(info.pos.cluster as usize);
let glyph = &info.glyph;
let top = ((PixelLength::new(self.render_metrics.cell_size.height as f64)
@ -1015,6 +1028,7 @@ impl super::TermWindow {
},
);
let right_fill_start = Instant::now();
for cell_idx in last_cell_idx + 1..num_cols {
// Even though we don't have a cell for these, they still
// hold the cursor or the selection so we need to compute
@ -1062,6 +1076,11 @@ impl super::TermWindow {
);
quad.set_cursor_color(params.cursor_border_color);
}
log::trace!(
"right fill {} -> elapsed {:?}",
num_cols.saturating_sub(last_cell_idx),
right_fill_start.elapsed()
);
Ok(())
}
@ -1277,7 +1296,7 @@ impl super::TermWindow {
) -> anyhow::Result<Vec<Rc<CachedGlyph<SrgbTexture2d>>>> {
let mut glyphs = Vec::with_capacity(infos.len());
for info in infos {
let cell_idx = cluster.byte_to_cell_idx[info.cluster as usize];
let cell_idx = cluster.byte_to_cell_idx(info.cluster as usize);
let followed_by_space = match line.cells().get(cell_idx + 1) {
Some(cell) => cell.str() == " ",
None => false,