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.
This commit is contained in:
@ -25,12 +25,13 @@ use tracing_error::ErrorLayer;
|
|||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
use super::{Buffer, BufferItem};
|
use super::{Buffer, BufferItem};
|
||||||
|
use crate::widgets::Prerender;
|
||||||
|
|
||||||
/// Maximum number of log lines to be stored in memory
|
/// Maximum number of log lines to be stored in memory
|
||||||
const MAX_MEM_LOG_LINES: usize = 100;
|
const MAX_MEM_LOG_LINES: usize = 100;
|
||||||
|
|
||||||
pub struct LogBuffer {
|
pub struct LogBuffer {
|
||||||
lines: VecDeque<String>,
|
lines: VecDeque<(String, Prerender)>,
|
||||||
receiver: UnboundedReceiver<String>,
|
receiver: UnboundedReceiver<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ impl Buffer for LogBuffer {
|
|||||||
if self.lines.len() >= MAX_MEM_LOG_LINES {
|
if self.lines.len() >= MAX_MEM_LOG_LINES {
|
||||||
self.lines.pop_front();
|
self.lines.pop_front();
|
||||||
}
|
}
|
||||||
self.lines.push_back(line);
|
self.lines.push_back((line, Prerender::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content<'a>(&'a self) -> Box<dyn Iterator<Item = BufferItem<'a>> + 'a> {
|
fn content<'a>(&'a self) -> Box<dyn Iterator<Item = BufferItem<'a>> + 'a> {
|
||||||
@ -69,12 +70,12 @@ impl Buffer for LogBuffer {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(slice2.into_iter())
|
.chain(slice2.into_iter())
|
||||||
.rev()
|
.rev()
|
||||||
.cloned()
|
.map(|(line, prerender)| BufferItem {
|
||||||
.map(|line| BufferItem {
|
text: line.clone().into_text().unwrap_or_else(|e| {
|
||||||
text: line.into_text().unwrap_or_else(|e| {
|
|
||||||
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
|
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
|
||||||
Text::raw(line)
|
Text::raw(line)
|
||||||
}),
|
}),
|
||||||
|
prerender,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::widgets::Prerender;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use matrix_sdk::async_trait;
|
use matrix_sdk::async_trait;
|
||||||
@ -25,8 +26,9 @@ pub use log::LogBuffer;
|
|||||||
mod room;
|
mod room;
|
||||||
pub use room::RoomBuffer;
|
pub use room::RoomBuffer;
|
||||||
|
|
||||||
pub struct BufferItem<'a> {
|
pub struct BufferItem<'buf> {
|
||||||
pub text: Text<'a>,
|
pub text: Text<'buf>,
|
||||||
|
pub prerender: &'buf Prerender,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -33,11 +33,12 @@ use ratatui::text::Text;
|
|||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use super::{Buffer, BufferItem};
|
use super::{Buffer, BufferItem};
|
||||||
|
use crate::widgets::Prerender;
|
||||||
|
|
||||||
pub struct SingleClientRoomBuffer {
|
pub struct SingleClientRoomBuffer {
|
||||||
room_id: OwnedRoomId,
|
room_id: OwnedRoomId,
|
||||||
client: Client,
|
client: Client,
|
||||||
items: imbl::vector::Vector<String>,
|
items: imbl::vector::Vector<(String, Prerender)>,
|
||||||
// TODO: get rid of this trait object, we know it's matrix_sdk_ui::timeline::TimelineStream
|
// TODO: get rid of this trait object, we know it's matrix_sdk_ui::timeline::TimelineStream
|
||||||
stream: Box<dyn Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + Send + Sync + Unpin>,
|
stream: Box<dyn Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + Send + Sync + Unpin>,
|
||||||
timeline: Arc<Timeline>,
|
timeline: Arc<Timeline>,
|
||||||
@ -54,7 +55,7 @@ impl SingleClientRoomBuffer {
|
|||||||
if let Some(changes) = self.stream.next().await {
|
if let Some(changes) = self.stream.next().await {
|
||||||
for change in changes {
|
for change in changes {
|
||||||
change
|
change
|
||||||
.map(|item| self.format_timeline_item(item))
|
.map(|item| (self.format_timeline_item(item), Prerender::new()))
|
||||||
.apply(&mut self.items);
|
.apply(&mut self.items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,7 +276,7 @@ impl RoomBuffer {
|
|||||||
timeline: Arc::new(timeline),
|
timeline: Arc::new(timeline),
|
||||||
items: items // FIXME: it's always empty. why?
|
items: items // FIXME: it's always empty. why?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| format!("Initial item: {:#?}", item))
|
.map(|item| (format!("Initial item: {:#?}", item), Prerender::new()))
|
||||||
.collect(),
|
.collect(),
|
||||||
stream: Box::new(stream),
|
stream: Box::new(stream),
|
||||||
back_pagination_request: AtomicU16::new(0),
|
back_pagination_request: AtomicU16::new(0),
|
||||||
@ -314,8 +315,9 @@ impl Buffer for RoomBuffer {
|
|||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.map(|line| BufferItem {
|
.map(|(line, prerender)| BufferItem {
|
||||||
text: Text::raw(line),
|
text: Text::raw(line),
|
||||||
|
prerender,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,18 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use super::Component;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
use color_eyre::eyre::{Result, WrapErr};
|
use color_eyre::eyre::{Result, WrapErr};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
|
||||||
use crate::components::Action;
|
use crate::components::Action;
|
||||||
|
use crate::widgets::prerender::{PrerenderInner, PrerenderValue};
|
||||||
use crate::widgets::{BottomAlignedParagraph, OverlappableWidget};
|
use crate::widgets::{BottomAlignedParagraph, OverlappableWidget};
|
||||||
|
|
||||||
|
use super::Component;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Backlog {
|
pub struct Backlog {
|
||||||
scroll: u64,
|
scroll: u64,
|
||||||
@ -71,8 +75,25 @@ impl Component for Backlog {
|
|||||||
let Some(item) = items.next() else {
|
let Some(item) = items.next() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let widget = BottomAlignedParagraph::new(item.text);
|
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);
|
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() {
|
if scroll.saturating_sub(expected_height) > text_area.height.into() {
|
||||||
// Paragraph is too far down, not displayed
|
// Paragraph is too far down, not displayed
|
||||||
@ -80,7 +101,8 @@ impl Component for Backlog {
|
|||||||
continue;
|
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());
|
let height = widget.render_overlap(text_area, frame.buffer_mut());
|
||||||
text_area.height = text_area.height.saturating_sub(height);
|
text_area.height = text_area.height.saturating_sub(height);
|
||||||
|
|
||||||
@ -96,8 +118,59 @@ impl Component for Backlog {
|
|||||||
|
|
||||||
// Render other widgets
|
// Render other widgets
|
||||||
for item in items {
|
for item in items {
|
||||||
|
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 widget = BottomAlignedParagraph::new(item.text);
|
||||||
let height = widget.render_overlap(text_area, frame.buffer_mut());
|
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);
|
assert!(area.height >= height, "{:?} {}", area, height);
|
||||||
text_area.height = text_area.height.saturating_sub(height); // Remove lines at the bottom used by this paragraph
|
text_area.height = text_area.height.saturating_sub(height); // Remove lines at the bottom used by this paragraph
|
||||||
if text_area.height == 0 {
|
if text_area.height == 0 {
|
||||||
|
@ -20,6 +20,9 @@ use ratatui::widgets::Widget;
|
|||||||
mod bottom_aligned_paragraph;
|
mod bottom_aligned_paragraph;
|
||||||
pub use bottom_aligned_paragraph::BottomAlignedParagraph;
|
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
|
#[rustfmt::skip] // reflow is vendored from ratatui, let's avoid changes
|
||||||
mod reflow;
|
mod reflow;
|
||||||
|
|
||||||
|
39
src/widgets/prerender.rs
Normal file
39
src/widgets/prerender.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Mutex<Option<PrerenderInner>>>);
|
||||||
|
|
||||||
|
impl Prerender {
|
||||||
|
pub fn new() -> Prerender {
|
||||||
|
Prerender(Arc::new(Mutex::new(None)))
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user