From 1199dd761328f43d9ae350f11cac77366fa884da Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 4 Nov 2023 22:15:48 +0100 Subject: [PATCH] Cache the result of rendering backlog items It's so much faster when getting lots of updates, especially with the 'Added client for' log storm at startup. --- src/buffers/log.rs | 11 ++--- src/buffers/mod.rs | 6 ++- src/buffers/room.rs | 10 +++-- src/components/backlog.rs | 85 ++++++++++++++++++++++++++++++++++++--- src/widgets/mod.rs | 3 ++ src/widgets/prerender.rs | 39 ++++++++++++++++++ 6 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 src/widgets/prerender.rs diff --git a/src/buffers/log.rs b/src/buffers/log.rs index ed82d96..99781ed 100644 --- a/src/buffers/log.rs +++ b/src/buffers/log.rs @@ -25,12 +25,13 @@ use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; use super::{Buffer, BufferItem}; +use crate::widgets::Prerender; /// Maximum number of log lines to be stored in memory const MAX_MEM_LOG_LINES: usize = 100; pub struct LogBuffer { - lines: VecDeque, + lines: VecDeque<(String, Prerender)>, receiver: UnboundedReceiver, } @@ -58,7 +59,7 @@ impl Buffer for LogBuffer { if self.lines.len() >= MAX_MEM_LOG_LINES { self.lines.pop_front(); } - self.lines.push_back(line); + self.lines.push_back((line, Prerender::new())); } fn content<'a>(&'a self) -> Box> + 'a> { @@ -69,12 +70,12 @@ impl Buffer for LogBuffer { .into_iter() .chain(slice2.into_iter()) .rev() - .cloned() - .map(|line| BufferItem { - text: line.into_text().unwrap_or_else(|e| { + .map(|(line, prerender)| BufferItem { + text: line.clone().into_text().unwrap_or_else(|e| { tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e); Text::raw(line) }), + prerender, }), ) } diff --git a/src/buffers/mod.rs b/src/buffers/mod.rs index dd58db8..85d2c6a 100644 --- a/src/buffers/mod.rs +++ b/src/buffers/mod.rs @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +use crate::widgets::Prerender; use futures::stream::FuturesUnordered; use futures::StreamExt; use matrix_sdk::async_trait; @@ -25,8 +26,9 @@ pub use log::LogBuffer; mod room; pub use room::RoomBuffer; -pub struct BufferItem<'a> { - pub text: Text<'a>, +pub struct BufferItem<'buf> { + pub text: Text<'buf>, + pub prerender: &'buf Prerender, } #[async_trait] diff --git a/src/buffers/room.rs b/src/buffers/room.rs index b54a312..a24d2f8 100644 --- a/src/buffers/room.rs +++ b/src/buffers/room.rs @@ -33,11 +33,12 @@ use ratatui::text::Text; use smallvec::SmallVec; use super::{Buffer, BufferItem}; +use crate::widgets::Prerender; pub struct SingleClientRoomBuffer { room_id: OwnedRoomId, client: Client, - items: imbl::vector::Vector, + items: imbl::vector::Vector<(String, Prerender)>, // TODO: get rid of this trait object, we know it's matrix_sdk_ui::timeline::TimelineStream stream: Box>>> + Send + Sync + Unpin>, timeline: Arc, @@ -54,7 +55,7 @@ impl SingleClientRoomBuffer { if let Some(changes) = self.stream.next().await { for change in changes { change - .map(|item| self.format_timeline_item(item)) + .map(|item| (self.format_timeline_item(item), Prerender::new())) .apply(&mut self.items); } } @@ -275,7 +276,7 @@ impl RoomBuffer { timeline: Arc::new(timeline), items: items // FIXME: it's always empty. why? .into_iter() - .map(|item| format!("Initial item: {:#?}", item)) + .map(|item| (format!("Initial item: {:#?}", item), Prerender::new())) .collect(), stream: Box::new(stream), back_pagination_request: AtomicU16::new(0), @@ -314,8 +315,9 @@ impl Buffer for RoomBuffer { .items .iter() .rev() - .map(|line| BufferItem { + .map(|(line, prerender)| BufferItem { text: Text::raw(line), + prerender, }), ) } diff --git a/src/components/backlog.rs b/src/components/backlog.rs index f4d03dc..e287c54 100644 --- a/src/components/backlog.rs +++ b/src/components/backlog.rs @@ -14,14 +14,18 @@ * along with this program. If not, see . */ -use super::Component; +use std::ops::DerefMut; + use color_eyre::eyre::{Result, WrapErr}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::{prelude::*, widgets::*}; use crate::components::Action; +use crate::widgets::prerender::{PrerenderInner, PrerenderValue}; use crate::widgets::{BottomAlignedParagraph, OverlappableWidget}; +use super::Component; + #[derive(Default)] pub struct Backlog { scroll: u64, @@ -71,8 +75,25 @@ impl Component for Backlog { let Some(item) = items.next() else { break; }; - let widget = BottomAlignedParagraph::new(item.text); - let expected_height = widget.height(text_area.width); + let expected_height = match item.prerender.0.lock().unwrap().deref_mut() { + Some(PrerenderInner { + key, + value: PrerenderValue::Rendered(buf), + }) if *key == text_area.width => buf.area().height as u64, + Some(PrerenderInner { + key, + value: PrerenderValue::NotRendered(height), + }) if *key == text_area.width => *height, + prerender => { + let widget = BottomAlignedParagraph::new(item.text.clone()); + let expected_height = widget.height(text_area.width); + *prerender = Some(PrerenderInner { + key: text_area.width, + value: PrerenderValue::NotRendered(expected_height), + }); + expected_height + }, + }; if scroll.saturating_sub(expected_height) > text_area.height.into() { // Paragraph is too far down, not displayed @@ -80,7 +101,8 @@ impl Component for Backlog { continue; } - let widget = widget.scroll(scroll); + // TODO: cache this + let widget = BottomAlignedParagraph::new(item.text).scroll(scroll); let height = widget.render_overlap(text_area, frame.buffer_mut()); text_area.height = text_area.height.saturating_sub(height); @@ -96,8 +118,59 @@ impl Component for Backlog { // Render other widgets for item in items { - let widget = BottomAlignedParagraph::new(item.text); - let height = widget.render_overlap(text_area, frame.buffer_mut()); + let height = match item.prerender.0.lock().unwrap().deref_mut() { + Some(PrerenderInner { + key, + value: PrerenderValue::Rendered(buf), + }) if *key == text_area.width => { + // We already rendered it, copy the buffer. + assert_eq!(buf.area().width, text_area.width); + for (y, line) in buf + .content() + .chunks(u16::min(buf.area().width, text_area.width) as usize) + .enumerate() + .skip(buf.area().height.saturating_sub(text_area.height) as usize) + { + for (x, cell) in line.into_iter().enumerate() { + *frame.buffer_mut().get_mut( + text_area.x + (x as u16), + text_area.y + text_area.height + (y as u16) - buf.area().height, + ) = cell.clone(); + } + } + + buf.area().height + }, + prerender => { + let widget = BottomAlignedParagraph::new(item.text); + let height = widget.render_overlap(text_area, frame.buffer_mut()); + + // Copy the drawn result to a buffer for caching + let mut buf = ratatui::buffer::Buffer::empty(Rect { + x: 0, + y: 0, + width: text_area.width, + height, + }); + for y in 0..height { + // TODO: only copy the width actually drawn by the widget + for x in 0..text_area.width { + *buf.get_mut(x, y) = frame + .buffer_mut() + .get(text_area.x + x, text_area.y + text_area.height + y - height) + .clone() + } + } + + *prerender = Some(PrerenderInner { + key: text_area.width, + value: PrerenderValue::Rendered(buf), + }); + + height + }, + }; + assert!(area.height >= height, "{:?} {}", area, height); text_area.height = text_area.height.saturating_sub(height); // Remove lines at the bottom used by this paragraph if text_area.height == 0 { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index bb3d88a..7eb5771 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -20,6 +20,9 @@ use ratatui::widgets::Widget; mod bottom_aligned_paragraph; pub use bottom_aligned_paragraph::BottomAlignedParagraph; +pub(crate) mod prerender; +pub use prerender::Prerender; + #[rustfmt::skip] // reflow is vendored from ratatui, let's avoid changes mod reflow; diff --git a/src/widgets/prerender.rs b/src/widgets/prerender.rs new file mode 100644 index 0000000..6be64b6 --- /dev/null +++ b/src/widgets/prerender.rs @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 Valentin Lorentz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use std::sync::{Arc, Mutex}; + +/// Storage for the result of pre-computations by the UI, with interior mutability +#[derive(Clone)] +pub(crate) struct PrerenderInner { + pub(crate) key: u16, // width + pub(crate) value: PrerenderValue, +} + +#[derive(Clone)] +pub(crate) enum PrerenderValue { + Rendered(ratatui::buffer::Buffer), + NotRendered(u64), // Height only. The widget was not rendered because off-screen. +} + +#[derive(Clone)] +pub struct Prerender(pub(crate) Arc>>); + +impl Prerender { + pub fn new() -> Prerender { + Prerender(Arc::new(Mutex::new(None))) + } +}