fix(Layout): use LruCache for layout cache (#487)

The layout cache now uses a LruCache with default size set to 16 entries.
Previously the cache was backed by a HashMap, and was able to grow
without bounds as a new entry was added for every new combination of
layout parameters.

- Added a new method (`layout::init_cache(usize)`) that allows the cache
size to be changed if necessary. This will only have an effect if it is called
prior to any calls to `layout::split()` as the cache is wrapped in a `OnceLock`
This commit is contained in:
Mariano Marciello 2023-09-15 00:20:38 +02:00 committed by GitHub
parent d4976d4b63
commit 638d596a3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 77 additions and 8 deletions

View file

@ -44,6 +44,7 @@ time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-width = "0.1"
document-features = { version = "0.2.7", optional = true }
lru = "0.11.1"
[dev-dependencies]
anyhow = "1.0.71"

View file

@ -3,7 +3,9 @@ use std::{
cmp::{max, min},
collections::HashMap,
fmt,
num::NonZeroUsize,
rc::Rc,
sync::OnceLock,
};
use cassowary::{
@ -12,6 +14,7 @@ use cassowary::{
WeightedRelation::{EQ, GE, LE},
};
use itertools::Itertools;
use lru::LruCache;
use strum::{Display, EnumString};
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
@ -337,6 +340,7 @@ impl Default for Layout {
}
impl Layout {
pub const DEFAULT_CACHE_SIZE: usize = 16;
/// Creates a new layout with default values.
///
/// - direction: [Direction::Vertical]
@ -355,6 +359,28 @@ impl Layout {
}
}
/// Initialize an empty cache with a custom size. The cache is keyed on the layout and area, so
/// that subsequent calls with the same parameters are faster. The cache is a LruCache, and
/// grows until `cache_size` is reached.
///
/// Returns true if the cell's value was set by this call.
/// Returns false if the cell's value was not set by this call, this means that another thread
/// has set this value or that the cache size is already initialized.
///
/// Note that a custom cache size will be set only if this function:
/// * is called before [Layout::split()] otherwise, the cache size is
/// [`Self::DEFAULT_CACHE_SIZE`].
/// * is called for the first time, subsequent calls do not modify the cache size.
pub fn init_cache(cache_size: usize) -> bool {
LAYOUT_CACHE
.with(|c| {
c.set(RefCell::new(LruCache::new(
NonZeroUsize::new(cache_size).unwrap(),
)))
})
.is_ok()
}
/// Builder method to set the constraints of the layout.
///
/// # Examples
@ -474,7 +500,8 @@ impl Layout {
///
/// This method stores the result of the computation in a thread-local cache keyed on the layout
/// and area, so that subsequent calls with the same parameters are faster. The cache is a
/// simple HashMap, and grows indefinitely (<https://github.com/ratatui-org/ratatui/issues/402>).
/// LruCache, and grows until [`Self::DEFAULT_CACHE_SIZE`] is reached by default, if the cache
/// is initialized with the [Layout::init_cache()] grows until the initialized cache size.
///
/// # Examples
///
@ -494,18 +521,21 @@ impl Layout {
/// ```
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
LAYOUT_CACHE.with(|c| {
c.borrow_mut()
.entry((area, self.clone()))
.or_insert_with(|| split(area, self))
.clone()
c.get_or_init(|| {
RefCell::new(LruCache::new(
NonZeroUsize::new(Self::DEFAULT_CACHE_SIZE).unwrap(),
))
})
.borrow_mut()
.get_or_insert((area, self.clone()), || split(area, self))
.clone()
})
}
}
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
type Cache = LruCache<(Rect, Layout), Rc<[Rect]>>;
thread_local! {
// TODO: Maybe use a fixed size cache https://github.com/ratatui-org/ratatui/issues/402
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
static LAYOUT_CACHE: OnceLock<RefCell<Cache>> = OnceLock::new();
}
/// A container used by the solver inside split
@ -667,6 +697,44 @@ mod tests {
use super::{SegmentSize::*, *};
use crate::prelude::Constraint::*;
#[test]
fn custom_cache_size() {
assert!(Layout::init_cache(10));
assert!(!Layout::init_cache(15));
LAYOUT_CACHE.with(|c| {
assert_eq!(c.get().unwrap().borrow().cap().get(), 10);
})
}
#[test]
fn default_cache_size() {
let target = Rect {
x: 2,
y: 2,
width: 10,
height: 10,
};
Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
]
.as_ref(),
)
.split(target);
assert!(!Layout::init_cache(15));
LAYOUT_CACHE.with(|c| {
assert_eq!(
c.get().unwrap().borrow().cap().get(),
Layout::DEFAULT_CACHE_SIZE
);
})
}
#[test]
fn corner_to_string() {
assert_eq!(Corner::BottomLeft.to_string(), "BottomLeft");