From b68f4ad5c68c91d76a23cc09f218c2e05fccdc1a Mon Sep 17 00:00:00 2001 From: koe Date: Sat, 31 Aug 2024 00:56:21 -0500 Subject: [PATCH] Optimize Buffer::set_rich_text for when the buffer is reconstructed --- src/attrs.rs | 7 ++ src/buffer.rs | 70 +++++++++---- src/buffer_line.rs | 101 +++++++++++++++---- src/cached.rs | 71 ++++++++++++++ src/lib.rs | 3 + src/shape.rs | 238 +++++++++++++++++++++++++++++++++++++++------ 6 files changed, 421 insertions(+), 69 deletions(-) create mode 100644 src/cached.rs diff --git a/src/attrs.rs b/src/attrs.rs index 046300c..782985b 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -373,4 +373,11 @@ impl AttrsList { } new } + + /// Resets the attributes with new defaults. + pub(crate) fn reset(mut self, default: Attrs) -> Self { + self.defaults = AttrsOwned::new(default); + self.spans.clear(); + self + } } diff --git a/src/buffer.rs b/src/buffer.rs index 94055f7..f573e45 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -115,8 +115,8 @@ impl<'b> Iterator for LayoutRunIter<'b> { fn next(&mut self) -> Option { while let Some(line) = self.buffer.lines.get(self.line_i) { - let shape = line.shape_opt().as_ref()?; - let layout = line.layout_opt().as_ref()?; + let shape = line.shape_opt()?; + let layout = line.layout_opt()?; while let Some(layout_line) = layout.get(self.layout_i) { self.layout_i += 1; @@ -725,11 +725,8 @@ impl Buffer { ) where I: IntoIterator)>, { - self.lines.clear(); - - let mut attrs_list = AttrsList::new(default_attrs); - let mut line_string = String::new(); let mut end = 0; + // TODO: find a way to cache this string and vec for reuse let (string, spans_data): (String, Vec<_>) = spans .into_iter() .map(|(s, attrs)| { @@ -753,15 +750,32 @@ impl Buffer { //TODO: set this based on information from spans let line_ending = LineEnding::default(); + let mut line_count = 0; + let mut attrs_list = self + .lines + .get_mut(line_count) + .map(BufferLine::reclaim_attrs) + .unwrap_or_else(|| AttrsList::new(Attrs::new())) + .reset(default_attrs); + let mut line_string = self + .lines + .get_mut(line_count) + .map(BufferLine::reclaim_text) + .unwrap_or_default(); + loop { let (Some(line_range), Some((attrs, span_range))) = (&maybe_line, &maybe_span) else { // this is reached only if this text is empty - self.lines.push(BufferLine::new( + if self.lines.len() <= line_count { + self.lines.push(BufferLine::empty()); + } + self.lines[line_count].reset_new( String::new(), line_ending, AttrsList::new(default_attrs), shaping, - )); + ); + line_count += 1; break; }; @@ -790,22 +804,44 @@ impl Buffer { maybe_line = lines_iter.next(); if maybe_line.is_some() { // finalize this line and start a new line - let prev_attrs_list = - core::mem::replace(&mut attrs_list, AttrsList::new(default_attrs)); - let prev_line_string = core::mem::take(&mut line_string); - let buffer_line = - BufferLine::new(prev_line_string, line_ending, prev_attrs_list, shaping); - self.lines.push(buffer_line); + let next_attrs_list = self + .lines + .get_mut(line_count + 1) + .map(BufferLine::reclaim_attrs) + .unwrap_or_else(|| AttrsList::new(Attrs::new())) + .reset(default_attrs); + let next_line_string = self + .lines + .get_mut(line_count + 1) + .map(BufferLine::reclaim_text) + .unwrap_or_default(); + let prev_attrs_list = core::mem::replace(&mut attrs_list, next_attrs_list); + let prev_line_string = core::mem::replace(&mut line_string, next_line_string); + if self.lines.len() <= line_count { + self.lines.push(BufferLine::empty()); + } + self.lines[line_count].reset_new( + prev_line_string, + line_ending, + prev_attrs_list, + shaping, + ); + line_count += 1; } else { // finalize the final line - let buffer_line = - BufferLine::new(line_string, line_ending, attrs_list, shaping); - self.lines.push(buffer_line); + if self.lines.len() <= line_count { + self.lines.push(BufferLine::empty()); + } + self.lines[line_count].reset_new(line_string, line_ending, attrs_list, shaping); + line_count += 1; break; } } } + // Discard excess lines now that we have reused as much of the existing allocations as possible. + self.lines.truncate(line_count); + self.scroll = Scroll::default(); self.shape_until_scroll(font_system, false); diff --git a/src/buffer_line.rs b/src/buffer_line.rs index 4f1a195..1ac71cb 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -2,7 +2,8 @@ use alloc::{string::String, vec::Vec}; use crate::{ - Align, AttrsList, FontSystem, LayoutLine, LineEnding, ShapeBuffer, ShapeLine, Shaping, Wrap, + Align, Attrs, AttrsList, Cached, FontSystem, LayoutLine, LineEnding, ShapeBuffer, ShapeLine, + Shaping, Wrap, }; /// A line (or paragraph) of text that is shaped and laid out @@ -12,8 +13,8 @@ pub struct BufferLine { ending: LineEnding, attrs_list: AttrsList, align: Option, - shape_opt: Option, - layout_opt: Option>, + shape_opt: Cached, + layout_opt: Cached>, shaping: Shaping, metadata: Option, } @@ -33,13 +34,33 @@ impl BufferLine { ending, attrs_list, align: None, - shape_opt: None, - layout_opt: None, + shape_opt: Cached::Empty, + layout_opt: Cached::Empty, shaping, metadata: None, } } + /// Resets the current line with new internal values. + /// + /// Avoids deallocating internal caches so they can be reused. + pub fn reset_new>( + &mut self, + text: T, + ending: LineEnding, + attrs_list: AttrsList, + shaping: Shaping, + ) { + self.text = text.into(); + self.ending = ending; + self.attrs_list = attrs_list; + self.align = None; + self.shape_opt.set_unused(); + self.layout_opt.set_unused(); + self.shaping = shaping; + self.metadata = None; + } + /// Get current text pub fn text(&self) -> &str { &self.text @@ -172,13 +193,13 @@ impl BufferLine { /// Reset shaping and layout caches pub fn reset_shaping(&mut self) { - self.shape_opt = None; + self.shape_opt.set_unused(); self.reset_layout(); } /// Reset only layout cache pub fn reset_layout(&mut self) { - self.layout_opt = None; + self.layout_opt.set_unused(); } /// Shape line, will cache results @@ -193,23 +214,28 @@ impl BufferLine { font_system: &mut FontSystem, tab_width: u16, ) -> &ShapeLine { - if self.shape_opt.is_none() { - self.shape_opt = Some(ShapeLine::new_in_buffer( + if self.shape_opt.is_unused() { + let mut line = self + .shape_opt + .take_unused() + .unwrap_or_else(ShapeLine::empty); + line.build_in_buffer( scratch, font_system, &self.text, &self.attrs_list, self.shaping, tab_width, - )); - self.layout_opt = None; + ); + self.shape_opt.set_used(line); + self.layout_opt.set_unused(); } - self.shape_opt.as_ref().expect("shape not found") + self.shape_opt.get().expect("shape not found") } /// Get line shaping cache - pub fn shape_opt(&self) -> &Option { - &self.shape_opt + pub fn shape_opt(&self) -> Option<&ShapeLine> { + self.shape_opt.get() } /// Layout line, will cache results @@ -244,10 +270,13 @@ impl BufferLine { match_mono_width: Option, tab_width: u16, ) -> &[LayoutLine] { - if self.layout_opt.is_none() { + if self.layout_opt.is_unused() { let align = self.align; + let mut layout = self + .layout_opt + .take_unused() + .unwrap_or_else(|| Vec::with_capacity(1)); let shape = self.shape_in_buffer(scratch, font_system, tab_width); - let mut layout = Vec::with_capacity(1); shape.layout_to_buffer( scratch, font_size, @@ -257,14 +286,14 @@ impl BufferLine { &mut layout, match_mono_width, ); - self.layout_opt = Some(layout); + self.layout_opt.set_used(layout); } - self.layout_opt.as_ref().expect("layout not found") + self.layout_opt.get().expect("layout not found") } /// Get line layout cache - pub fn layout_opt(&self) -> &Option> { - &self.layout_opt + pub fn layout_opt(&self) -> Option<&Vec> { + self.layout_opt.get() } /// Get line metadata. This will be None if [`BufferLine::set_metadata`] has not been called @@ -277,4 +306,36 @@ impl BufferLine { pub fn set_metadata(&mut self, metadata: usize) { self.metadata = Some(metadata); } + + /// Makes an empty buffer line. + /// + /// The buffer line is in an invalid state after this is called. See [`Self::reclaim_new`]. + pub(crate) fn empty() -> Self { + Self { + text: String::default(), + ending: LineEnding::default(), + attrs_list: AttrsList::new(Attrs::new()), + align: None, + shape_opt: Cached::Empty, + layout_opt: Cached::Empty, + shaping: Shaping::Advanced, + metadata: None, + } + } + + /// Reclaim attributes list memory that isn't needed any longer. + /// + /// The buffer line is in an invalid state after this is called. See [`Self::reclaim_new`]. + pub(crate) fn reclaim_attrs(&mut self) -> AttrsList { + std::mem::replace(&mut self.attrs_list, AttrsList::new(Attrs::new())) + } + + /// Reclaim text memory that isn't needed any longer. + /// + /// The buffer line is in an invalid state after this is called. See [`Self::reclaim_new`]. + pub(crate) fn reclaim_text(&mut self) -> String { + let mut text = std::mem::take(&mut self.text); + text.clear(); + text + } } diff --git a/src/cached.rs b/src/cached.rs new file mode 100644 index 0000000..38c5eb8 --- /dev/null +++ b/src/cached.rs @@ -0,0 +1,71 @@ +use core::fmt::Debug; + +/// Helper for caching a value when the value is optionally present in the 'unused' state. +#[derive(Clone, Debug)] +pub enum Cached { + Empty, + Unused(T), + Used(T), +} + +impl Cached { + pub fn get(&self) -> Option<&T> { + match self { + Self::Empty | Self::Unused(_) => None, + Self::Used(t) => Some(t), + } + } + + pub fn get_mut(&mut self) -> Option<&mut T> { + match self { + Self::Empty | Self::Unused(_) => None, + Self::Used(t) => Some(t), + } + } + + pub fn is_unused(&self) -> bool { + match self { + Self::Empty | Self::Unused(_) => true, + Self::Used(_) => false, + } + } + + pub fn is_used(&self) -> bool { + match self { + Self::Empty | Self::Unused(_) => false, + Self::Used(_) => true, + } + } + + pub fn take_unused(&mut self) -> Option { + if matches!(*self, Self::Unused(_)) { + let Self::Unused(val) = std::mem::replace(self, Self::Empty) else { + unreachable!() + }; + Some(val) + } else { + None + } + } + + pub fn take_used(&mut self) -> Option { + if matches!(*self, Self::Used(_)) { + let Self::Used(val) = std::mem::replace(self, Self::Empty) else { + unreachable!() + }; + Some(val) + } else { + None + } + } + + pub fn set_unused(&mut self) { + if matches!(*self, Self::Used(_)) { + *self = Self::Unused(self.take_used().unwrap()); + } + } + + pub fn set_used(&mut self, val: T) { + *self = Self::Used(val); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0efcc8e..8a4fbab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,9 @@ mod buffer; pub use self::buffer_line::*; mod buffer_line; +pub use self::cached::*; +mod cached; + pub use self::glyph_cache::*; mod glyph_cache; diff --git a/src/shape.rs b/src/shape.rs index e294f24..b13ec1e 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -87,8 +87,18 @@ pub struct ShapeBuffer { /// Temporary buffers for scripts. scripts: Vec