use super::{LayoutKey, TextCaretPosition, TextLayout, TextLine};
use incular_rendering::{GlyphPosition, GlyphRun};
use lru::LruCache;
use parley::Layout;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
mem::size_of,
num::NonZeroUsize,
sync::Arc,
};
pub(super) const LAYOUT_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(2048).unwrap();
const MAX_RETAINED_LAYOUTS: usize = 1 << 16;
pub(super) const DEFAULT_CACHE_BYTE_BUDGET: usize = 32 << 20;
const DECAY_WINDOW: u32 = 1024;
const ENTRY_OVERHEAD: usize = 64;
const PARLEY_BYTES_PER_TEXT_BYTE: usize = 72;
pub(super) trait RetainedBytes {
fn retained_bytes(&self, key: &LayoutKey<'_>) -> usize;
}
impl RetainedBytes for Arc<TextLayout> {
fn retained_bytes(&self, _key: &LayoutKey<'_>) -> usize {
let layout = &**self;
let mut bytes = size_of::<TextLayout>()
+ layout.lines.len() * size_of::<TextLine>()
+ layout.font_runs.len() * size_of::<super::FontRunDebug>();
for line in layout.lines.iter() {
bytes += line.runs.len() * size_of::<Arc<GlyphRun>>()
+ line.clusters.len() * size_of::<super::TextClusterSpan>();
for run in line.runs.iter() {
bytes += size_of::<GlyphRun>() + run.glyphs.len() * size_of::<GlyphPosition>();
}
if !line
.runs
.first()
.is_some_and(|run| Arc::ptr_eq(&run.glyphs, &line.glyphs))
{
bytes += line.glyphs.len() * size_of::<GlyphPosition>();
}
}
if let Some(carets) = layout.caret_positions.get() {
bytes += carets.len() * size_of::<TextCaretPosition>();
}
bytes
}
}
impl RetainedBytes for Layout<()> {
fn retained_bytes(&self, key: &LayoutKey<'_>) -> usize {
size_of::<Self>() + key.text.len() * PARLEY_BYTES_PER_TEXT_BYTE
}
}
struct Entry<V> {
key: LayoutKey<'static>,
value: V,
bytes: usize,
window: u64,
}
pub(super) struct LayoutPool<V = Arc<TextLayout>> {
entries: LruCache<u64, Entry<V>>,
evicted: LruCache<u64, ()>,
pub(super) growths: u64,
pub(super) shrinks: u64,
bytes: usize,
byte_budget: usize,
pinned: usize,
pinned_this_window: bool,
window: u64,
window_requests: u32,
window_lookups: u32,
window_hits: u32,
window_touched: u32,
grew_this_window: bool,
}
impl<V: RetainedBytes> LayoutPool<V> {
pub(super) fn new(byte_budget: usize) -> Self {
Self {
entries: LruCache::new(LAYOUT_CACHE_CAPACITY),
evicted: LruCache::new(LAYOUT_CACHE_CAPACITY),
growths: 0,
shrinks: 0,
bytes: 0,
byte_budget,
pinned: 0,
pinned_this_window: false,
window: 0,
window_requests: 0,
window_lookups: 0,
window_hits: 0,
window_touched: 0,
grew_this_window: false,
}
}
pub(super) fn get(&mut self, key: &LayoutKey<'_>) -> Option<V>
where
V: Clone,
{
self.window_lookups = self.window_lookups.saturating_add(1);
let hash = key_hash(key);
if let Some(entry) = self.entries.get_mut(&hash)
&& entry.key == *key
{
self.window_hits = self.window_hits.saturating_add(1);
if entry.window != self.window {
entry.window = self.window;
self.window_touched = self.window_touched.saturating_add(1);
}
return Some(entry.value.clone());
}
self.note_miss(hash);
None
}
pub(super) fn take(&mut self, key: &LayoutKey<'_>) -> Option<V> {
self.window_lookups = self.window_lookups.saturating_add(1);
let hash = key_hash(key);
if self
.entries
.peek(&hash)
.is_none_or(|entry| entry.key != *key)
{
self.note_miss(hash);
return None;
}
let entry = self.entries.pop(&hash)?;
self.window_hits = self.window_hits.saturating_add(1);
if entry.window != self.window {
self.window_touched = self.window_touched.saturating_add(1);
}
self.bytes -= entry.bytes;
Some(entry.value)
}
fn note_miss(&mut self, hash: u64) {
if self.evicted.pop(&hash).is_some() {
let doubled = self.entries.cap().get().saturating_mul(2);
if self.reserve(doubled.min(MAX_RETAINED_LAYOUTS)) {
self.growths += 1;
self.grew_this_window = true;
}
}
}
pub(super) fn put(&mut self, key: LayoutKey<'_>, value: V) {
let hash = key_hash(&key);
let key = key.into_owned();
let bytes =
key.text.len() + size_of::<Entry<V>>() + ENTRY_OVERHEAD + value.retained_bytes(&key);
self.bytes += bytes;
let entry = Entry {
key,
value,
bytes,
window: self.window,
};
if let Some((evicted, entry)) = self.entries.push(hash, entry) {
self.bytes -= entry.bytes;
if !self.entries.contains(&evicted) {
self.evicted.put(evicted, ());
}
}
while self.bytes > self.byte_budget && self.entries.len() > self.pinned.max(1) {
self.pop_lru();
}
}
fn pop_lru(&mut self) {
if let Some((_, entry)) = self.entries.pop_lru() {
self.bytes -= entry.bytes;
}
}
pub(super) fn reserve(&mut self, capacity: usize) -> bool {
match NonZeroUsize::new(capacity) {
Some(capacity) if capacity > self.entries.cap() => {
self.entries.resize(capacity);
self.evicted.resize(capacity);
true
}
_ => false,
}
}
pub(super) fn pin(&mut self, entries: usize) {
self.pinned = self.pinned.max(entries);
self.pinned_this_window = true;
}
pub(super) fn tick(&mut self) {
self.window_requests += 1;
if self.window_requests < DECAY_WINDOW {
return;
}
let (lookups, hits, touched) = (
self.window_lookups as usize,
self.window_hits as usize,
self.window_touched as usize,
);
let oversized = !self.grew_this_window
&& hits * 20 >= lookups * 19
&& touched * 2 <= lookups
&& self.capacity() > LAYOUT_CACHE_CAPACITY.get();
if oversized {
let floor = if self.pinned > 0 {
self.pinned + LAYOUT_CACHE_CAPACITY.get()
} else {
LAYOUT_CACHE_CAPACITY.get()
};
let target = (touched * 4).max(floor).next_power_of_two();
if target < self.capacity() {
self.shrink(target);
}
}
if !self.pinned_this_window {
self.pinned = 0;
}
self.pinned_this_window = false;
self.grew_this_window = false;
self.window += 1;
self.window_requests = 0;
self.window_lookups = 0;
self.window_hits = 0;
self.window_touched = 0;
}
fn shrink(&mut self, capacity: usize) {
while self.entries.len() > capacity {
self.pop_lru();
}
let capacity = NonZeroUsize::new(capacity).unwrap_or(LAYOUT_CACHE_CAPACITY);
self.entries.resize(capacity);
self.evicted.resize(capacity);
self.shrinks += 1;
}
pub(super) fn capacity(&self) -> usize {
self.entries.cap().get()
}
pub(super) const fn bytes(&self) -> usize {
self.bytes
}
pub(super) fn set_byte_budget(&mut self, bytes: usize) {
self.byte_budget = bytes;
while self.bytes > self.byte_budget && self.entries.len() > self.pinned.max(1) {
self.pop_lru();
}
}
pub(super) fn clear(&mut self) {
self.entries.clear();
self.evicted.clear();
self.bytes = 0;
}
}
fn key_hash(key: &LayoutKey<'_>) -> u64 {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
hasher.finish()
}