mirror of
https://github.com/wez/wezterm.git
synced 2024-12-22 21:01:36 +03:00
lfucache: improve LFU algorithm and structure
Do it "more properly": use intrusive list linkage to manage three indices: * hash lookup * recency * frequency eviction is frequency based, but in order to avoid things that were super hot in the past and that are no longer hot clogging up the cache, eviction will incrementally age out least recently used entries that haven't been active in the span of some number of get/put operations. Aging scales down the frequency value to reduce its strength, so an aged item isn't necessarily immediately a candidate for removal, it just makes it more likely to be picked up by the frequency based removal as it goes unused for an extended period.
This commit is contained in:
parent
72757b8849
commit
0516b61f62
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -34,6 +34,18 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"getrandom 0.2.8",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.19"
|
||||
@ -877,7 +889,7 @@ dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
@ -1720,7 +1732,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"gl_generator",
|
||||
"lazy_static",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
"smallvec",
|
||||
"takeable-option",
|
||||
]
|
||||
@ -1824,7 +1836,7 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.7.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1833,7 +1845,7 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.7.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2095,6 +2107,15 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intrusive-collections"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe531a7789d7120f3e17d4f3f2cd95f54418ba7354f60b7b622b6644a07888a"
|
||||
dependencies = [
|
||||
"memoffset 0.5.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ioctl-rs"
|
||||
version = "0.1.6"
|
||||
@ -2245,8 +2266,11 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
name = "lfucache"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.2",
|
||||
"config",
|
||||
"fnv",
|
||||
"intrusive-collections",
|
||||
"k9",
|
||||
"metrics",
|
||||
]
|
||||
|
||||
@ -2534,6 +2558,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.6.5"
|
||||
@ -2549,7 +2582,7 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55586aa936c35f34ba8aa5d97356d554311206e1ce1f9e68fe7b07288e5ad827"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ahash 0.7.6",
|
||||
"metrics-macros",
|
||||
]
|
||||
|
||||
@ -2748,7 +2781,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2761,7 +2794,7 @@ dependencies = [
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2773,7 +2806,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2786,7 +2819,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
@ -5513,6 +5546,7 @@ dependencies = [
|
||||
"harfbuzz",
|
||||
"k9",
|
||||
"lazy_static",
|
||||
"lfucache",
|
||||
"log",
|
||||
"memmap2 0.2.3",
|
||||
"metrics",
|
||||
|
@ -6,6 +6,11 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.8"
|
||||
config = { path = "../config" }
|
||||
fnv = "1.0"
|
||||
intrusive-collections = "0.9"
|
||||
metrics = { version="0.17", features=["std"]}
|
||||
|
||||
[dev-dependencies]
|
||||
k9 = "0.11"
|
||||
|
@ -1,60 +1,91 @@
|
||||
use ahash::AHasher;
|
||||
use config::ConfigHandle;
|
||||
use fnv::FnvHashMap;
|
||||
use intrusive_collections::{
|
||||
intrusive_adapter, Bound, KeyAdapter, LinkedList, LinkedListLink, RBTree, RBTreeLink,
|
||||
};
|
||||
use std::borrow::Borrow;
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::Eq;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::fmt::Debug;
|
||||
use std::hash::{BuildHasher, BuildHasherDefault, Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub type CapFunc = fn(&ConfigHandle) -> usize;
|
||||
|
||||
struct ValueWithFreq<V> {
|
||||
struct Entry<K, V> {
|
||||
hash_link: LinkedListLink,
|
||||
recency_link: LinkedListLink,
|
||||
frequency_link: RBTreeLink,
|
||||
freq: RefCell<u16>,
|
||||
last_tick: RefCell<u32>,
|
||||
key: K,
|
||||
value: V,
|
||||
freq: u16,
|
||||
}
|
||||
|
||||
impl<'a, V: 'a> ValueWithFreq<V> {
|
||||
/// A very basic LFU algorithm.
|
||||
/// If we have a known latest key, just return it.
|
||||
/// Otherwise, find the key with the lowest freq by simply
|
||||
/// iterating the entire cache.
|
||||
/// For large cache sizes, this isn't great.
|
||||
pub fn lfu<K: Clone + 'a>(
|
||||
latest: &mut Option<K>,
|
||||
iter: impl Iterator<Item = (&'a K, &'a ValueWithFreq<V>)>,
|
||||
) -> Option<K> {
|
||||
if let Some(key) = latest.take() {
|
||||
return Some(key);
|
||||
}
|
||||
let mut lfu = None;
|
||||
for (k, ValueWithFreq { freq, .. }) in iter {
|
||||
if let Some((other_key, other_freq)) = lfu.take() {
|
||||
if freq < other_freq {
|
||||
lfu.replace((k, freq));
|
||||
} else {
|
||||
lfu.replace((other_key, other_freq));
|
||||
}
|
||||
} else {
|
||||
lfu.replace((k, freq));
|
||||
}
|
||||
}
|
||||
intrusive_adapter!(RecencyAdapter<K,V> = Rc<Entry<K,V>>: Entry<K,V> { recency_link: LinkedListLink });
|
||||
intrusive_adapter!(FrequenceAdapter<K,V> = Rc<Entry<K,V>>: Entry<K,V> { frequency_link: RBTreeLink });
|
||||
intrusive_adapter!(HashAdapter<K,V> = Rc<Entry<K,V>>: Entry<K,V> { hash_link: LinkedListLink });
|
||||
|
||||
lfu.map(|(k, _)| k.clone())
|
||||
/// Key by Entry::freq
|
||||
impl<'a, K, V> KeyAdapter<'a> for FrequenceAdapter<K, V> {
|
||||
type Key = u16;
|
||||
fn get_key(&self, entry: &'a Entry<K, V>) -> u16 {
|
||||
*entry.freq.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
pub type CapFunc = fn(&ConfigHandle) -> usize;
|
||||
|
||||
/// A cache using a Least-Frequently-Used eviction policy.
|
||||
/// If K is u64 you should use LfuCacheU64 instead as it has
|
||||
/// less overhead.
|
||||
pub struct LfuCache<K, V> {
|
||||
/// a more optimal hasher for integer keys.
|
||||
pub struct LfuCache<K, V, S = BuildHasherDefault<AHasher>> {
|
||||
hit: &'static str,
|
||||
miss: &'static str,
|
||||
map: HashMap<K, ValueWithFreq<V>>,
|
||||
cap: usize,
|
||||
cap_func: CapFunc,
|
||||
latest: Option<K>,
|
||||
hasher: S,
|
||||
|
||||
/// hash buckets for key-based lookup
|
||||
buckets: Vec<LinkedList<HashAdapter<K, V>>>,
|
||||
/// frequency-keyed rb-tree
|
||||
frequency_index: RBTree<FrequenceAdapter<K, V>>,
|
||||
/// the back is the least-recently-used whereas the front is the
|
||||
/// most-recently-used
|
||||
recency_index: LinkedList<RecencyAdapter<K, V>>,
|
||||
/// Number of items in the cache
|
||||
len: usize,
|
||||
/// tracks number of operations that affect the frequency/age of entries
|
||||
tick: u32,
|
||||
}
|
||||
|
||||
impl<K: Hash + Eq + Clone, V> LfuCache<K, V> {
|
||||
impl<K: Hash + Eq + Clone + Debug, V, S: Default + BuildHasher> LfuCache<K, V, S> {
|
||||
#[cfg(test)]
|
||||
fn with_capacity(cap: usize) -> Self {
|
||||
let mut buckets = vec![];
|
||||
let num_buckets = (cap / 10).next_power_of_two();
|
||||
for _ in 0..num_buckets {
|
||||
buckets.push(LinkedList::new(HashAdapter::new()));
|
||||
}
|
||||
|
||||
let hasher = S::default();
|
||||
|
||||
fn dummy_cap_func(_: &ConfigHandle) -> usize {
|
||||
8
|
||||
}
|
||||
|
||||
Self {
|
||||
hit: "hit",
|
||||
miss: "miss",
|
||||
cap,
|
||||
cap_func: dummy_cap_func,
|
||||
buckets,
|
||||
frequency_index: RBTree::new(FrequenceAdapter::new()),
|
||||
recency_index: LinkedList::new(RecencyAdapter::new()),
|
||||
len: 0,
|
||||
tick: 0,
|
||||
hasher,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
hit: &'static str,
|
||||
miss: &'static str,
|
||||
@ -62,165 +93,731 @@ impl<K: Hash + Eq + Clone, V> LfuCache<K, V> {
|
||||
config: &ConfigHandle,
|
||||
) -> Self {
|
||||
let cap = cap_func(config);
|
||||
let mut buckets = vec![];
|
||||
let num_buckets = (cap / 10).next_power_of_two();
|
||||
for _ in 0..num_buckets {
|
||||
buckets.push(LinkedList::new(HashAdapter::new()));
|
||||
}
|
||||
|
||||
let hasher = S::default();
|
||||
|
||||
Self {
|
||||
hit,
|
||||
miss,
|
||||
map: HashMap::with_capacity(cap),
|
||||
cap,
|
||||
cap_func,
|
||||
latest: None,
|
||||
buckets,
|
||||
frequency_index: RBTree::new(FrequenceAdapter::new()),
|
||||
recency_index: LinkedList::new(RecencyAdapter::new()),
|
||||
len: 0,
|
||||
tick: 0,
|
||||
hasher,
|
||||
}
|
||||
}
|
||||
|
||||
fn bucket_for_key<Q: Hash>(&self, k: &Q) -> usize {
|
||||
let mut hasher = self.hasher.build_hasher();
|
||||
k.hash(&mut hasher);
|
||||
(hasher.finish() as usize) % self.buckets.len()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
self.len
|
||||
}
|
||||
|
||||
/// Grow the hash buckets in the pursuit of reducing potential
|
||||
/// key collisions in any given bucket
|
||||
fn grow_hash(&mut self) {
|
||||
let num_buckets = self.buckets.len() * 2;
|
||||
let mut buckets = vec![];
|
||||
for _ in 0..num_buckets {
|
||||
buckets.push(LinkedList::new(HashAdapter::new()));
|
||||
}
|
||||
std::mem::swap(&mut buckets, &mut self.buckets);
|
||||
|
||||
for mut old_bucket in buckets {
|
||||
while let Some(entry) = old_bucket.pop_front() {
|
||||
let bucket = self.bucket_for_key(&entry.key);
|
||||
self.buckets[bucket].push_front(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &ConfigHandle) {
|
||||
let new_cap = (self.cap_func)(config);
|
||||
if new_cap != self.cap {
|
||||
self.cap = new_cap;
|
||||
self.map = HashMap::with_capacity(new_cap);
|
||||
while self.len > self.cap {
|
||||
self.evict_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In order to mitigate previously-very-hot entries that are
|
||||
/// not currently being accessed from occupying the bulk of the
|
||||
/// table, this function finds the least-recently-accessed item
|
||||
/// and decreases its frequency value based on the number of ticks
|
||||
/// that have occurred since its last use.
|
||||
fn decay_least_recent(&mut self) {
|
||||
let mut cursor = self.recency_index.back_mut();
|
||||
if let Some(entry) = cursor.get() {
|
||||
if *entry.freq.borrow() == 0 {
|
||||
// No point removing/reinserting in the rbtree if the freq
|
||||
// is already 0
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = ((self.tick - *entry.last_tick.borrow()) / 10) as u16;
|
||||
if delta <= 1 {
|
||||
// No point removing/reinserting in the rbtree if there is no change
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust lfu
|
||||
unsafe {
|
||||
let lfu_entry = self
|
||||
.frequency_index
|
||||
.cursor_mut_from_ptr(entry)
|
||||
.remove()
|
||||
.unwrap();
|
||||
{
|
||||
let mut freq = entry.freq.borrow_mut();
|
||||
*freq = *freq / delta;
|
||||
}
|
||||
self.frequency_index.insert(lfu_entry);
|
||||
}
|
||||
|
||||
// Adjust lru so that we don't immediately revisit this one
|
||||
// on the next decay_least_recent() call
|
||||
let lru_entry = cursor.remove().unwrap();
|
||||
self.recency_index.push_front(lru_entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the entry with the smallest frequency value
|
||||
fn evict_one(&mut self) {
|
||||
self.decay_least_recent();
|
||||
|
||||
let mut cursor = self.frequency_index.lower_bound_mut(Bound::Included(&0));
|
||||
if let Some(entry) = cursor.remove() {
|
||||
let bucket = self.bucket_for_key(&entry.key);
|
||||
unsafe {
|
||||
self.buckets
|
||||
.get_mut(bucket)
|
||||
.unwrap()
|
||||
.cursor_mut_from_ptr(&*entry)
|
||||
.remove();
|
||||
self.recency_index.cursor_mut_from_ptr(&*entry).remove();
|
||||
}
|
||||
self.len -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
self.frequency_index.clear();
|
||||
self.recency_index.clear();
|
||||
for bucket in &mut self.buckets {
|
||||
bucket.clear();
|
||||
}
|
||||
self.len = 0;
|
||||
}
|
||||
|
||||
pub fn get<'a, Q: ?Sized>(&'a mut self, k: &Q) -> Option<&'a V>
|
||||
pub fn get<'a, Q: ?Sized + Debug>(&'a mut self, k: &Q) -> Option<&'a V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: Hash + Eq,
|
||||
{
|
||||
match self.map.get_mut(k) {
|
||||
None => {
|
||||
metrics::histogram!(self.miss, 1.);
|
||||
None
|
||||
}
|
||||
Some(ValueWithFreq { value, freq }) => {
|
||||
metrics::histogram!(self.hit, 1.);
|
||||
*freq = freq.saturating_add(1);
|
||||
match &self.latest {
|
||||
Some(latest) if latest.borrow() == k => {
|
||||
self.latest.take();
|
||||
}
|
||||
_ => {}
|
||||
let bucket = self.bucket_for_key(&k);
|
||||
let mut cursor = self.buckets.get_mut(bucket)?.front_mut();
|
||||
while let Some(entry) = cursor.get() {
|
||||
if entry.key.borrow() == k {
|
||||
// Adjust lru
|
||||
unsafe {
|
||||
let lru_entry = self
|
||||
.recency_index
|
||||
.cursor_mut_from_ptr(entry)
|
||||
.remove()
|
||||
.unwrap();
|
||||
self.recency_index.push_front(lru_entry);
|
||||
}
|
||||
Some(value)
|
||||
|
||||
let entry = cursor.into_ref()?;
|
||||
metrics::histogram!(self.hit, 1.);
|
||||
|
||||
self.tick += 1;
|
||||
|
||||
*entry.last_tick.borrow_mut() = self.tick;
|
||||
|
||||
// Adjust lfu
|
||||
unsafe {
|
||||
let lfu_entry = self
|
||||
.frequency_index
|
||||
.cursor_mut_from_ptr(entry)
|
||||
.remove()
|
||||
.unwrap();
|
||||
{
|
||||
let mut freq = lfu_entry.freq.borrow_mut();
|
||||
*freq = freq.saturating_add(1);
|
||||
}
|
||||
self.frequency_index.insert(lfu_entry);
|
||||
}
|
||||
|
||||
return Some(&entry.value);
|
||||
}
|
||||
|
||||
cursor.move_next();
|
||||
}
|
||||
metrics::histogram!(self.miss, 1.);
|
||||
None
|
||||
}
|
||||
|
||||
pub fn put(&mut self, k: K, v: V) -> Option<V> {
|
||||
let prior = self.map.remove(&k);
|
||||
if self.map.len() >= self.cap {
|
||||
let lfu = ValueWithFreq::lfu(&mut self.latest, self.map.iter());
|
||||
if let Some(key) = lfu {
|
||||
self.map.remove(&key);
|
||||
pub fn put(&mut self, k: K, v: V) {
|
||||
let bucket = self.bucket_for_key(&k);
|
||||
|
||||
self.tick += 1;
|
||||
|
||||
// Remove any prior value
|
||||
{
|
||||
let mut cursor = self
|
||||
.buckets
|
||||
.get_mut(bucket)
|
||||
.expect("valid bucket index")
|
||||
.front_mut();
|
||||
while let Some(entry) = cursor.get() {
|
||||
if entry.key == k {
|
||||
unsafe {
|
||||
self.frequency_index.cursor_mut_from_ptr(entry).remove();
|
||||
self.recency_index.cursor_mut_from_ptr(entry).remove();
|
||||
}
|
||||
cursor.remove();
|
||||
self.len -= 1;
|
||||
break;
|
||||
}
|
||||
cursor.move_next();
|
||||
}
|
||||
}
|
||||
self.latest.replace(k.clone());
|
||||
self.map.insert(k, ValueWithFreq { value: v, freq: 0 });
|
||||
prior.map(|ent| ent.value)
|
||||
|
||||
while self.len >= self.cap {
|
||||
self.evict_one();
|
||||
}
|
||||
|
||||
let entry = Rc::new(Entry {
|
||||
key: k,
|
||||
value: v,
|
||||
freq: RefCell::new(0),
|
||||
recency_link: LinkedListLink::new(),
|
||||
frequency_link: RBTreeLink::new(),
|
||||
hash_link: LinkedListLink::new(),
|
||||
last_tick: RefCell::new(self.tick),
|
||||
});
|
||||
self.buckets[bucket].push_front(Rc::clone(&entry));
|
||||
self.frequency_index.insert(Rc::clone(&entry));
|
||||
self.recency_index.push_front(entry);
|
||||
self.len += 1;
|
||||
if self.buckets.len() < self.cap && self.len > self.buckets.len() / 2 {
|
||||
self.grow_hash();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cache using a Least-Frequently-Used eviction policy, where the cache keys
|
||||
/// are u64
|
||||
pub struct LfuCacheU64<V> {
|
||||
hit: &'static str,
|
||||
miss: &'static str,
|
||||
map: FnvHashMap<u64, ValueWithFreq<V>>,
|
||||
cap: usize,
|
||||
cap_func: CapFunc,
|
||||
latest: Option<u64>,
|
||||
}
|
||||
/// A cache using a Least-Frequently-Used eviction policy,
|
||||
/// where the cache keys are u64
|
||||
pub type LfuCacheU64<V> = LfuCache<u64, V, fnv::FnvBuildHasher>;
|
||||
|
||||
impl<V> LfuCacheU64<V> {
|
||||
pub fn new(
|
||||
hit: &'static str,
|
||||
miss: &'static str,
|
||||
cap_func: CapFunc,
|
||||
config: &ConfigHandle,
|
||||
) -> Self {
|
||||
let cap = cap_func(config);
|
||||
Self {
|
||||
hit,
|
||||
miss,
|
||||
map: FnvHashMap::default(),
|
||||
cap,
|
||||
cap_func,
|
||||
latest: None,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct EntryData<'a, K, V> {
|
||||
freq: u16,
|
||||
last_tick: u32,
|
||||
key: &'a K,
|
||||
value: &'a V,
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &ConfigHandle) {
|
||||
let new_cap = (self.cap_func)(config);
|
||||
if new_cap != self.cap {
|
||||
self.cap = new_cap;
|
||||
self.map = FnvHashMap::default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
pub fn get(&mut self, id: &u64) -> Option<&V> {
|
||||
match self.map.get_mut(&id) {
|
||||
None => {
|
||||
metrics::histogram!(self.miss, 1.0);
|
||||
None
|
||||
}
|
||||
Some(ValueWithFreq { value, freq }) => {
|
||||
metrics::histogram!(self.hit, 1.);
|
||||
*freq = freq.saturating_add(1);
|
||||
match &self.latest {
|
||||
Some(latest) if latest == id => {
|
||||
self.latest.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(value)
|
||||
impl<'a, K, V> EntryData<'a, K, V> {
|
||||
fn new(item: &'a Entry<K, V>) -> Self {
|
||||
Self {
|
||||
freq: *item.freq.borrow(),
|
||||
last_tick: *item.last_tick.borrow(),
|
||||
key: &item.key,
|
||||
value: &item.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: &u64) -> Option<&mut V> {
|
||||
match self.map.get_mut(&id) {
|
||||
None => {
|
||||
metrics::histogram!(self.miss, 1.0);
|
||||
None
|
||||
}
|
||||
Some(ValueWithFreq { value, freq }) => {
|
||||
metrics::histogram!(self.hit, 1.);
|
||||
*freq = freq.saturating_add(1);
|
||||
match &self.latest {
|
||||
Some(latest) if latest == id => {
|
||||
self.latest.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
fn frequency_order<K, V, S>(cache: &LfuCache<K, V, S>) -> Vec<EntryData<K, V>> {
|
||||
let mut entries = vec![];
|
||||
for item in cache.frequency_index.iter() {
|
||||
entries.push(EntryData::new(item));
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
pub fn put(&mut self, id: u64, v: V) -> Option<V> {
|
||||
let prior = self.map.remove(&id);
|
||||
if self.map.len() >= self.cap {
|
||||
let lfu = ValueWithFreq::lfu(&mut self.latest, self.map.iter());
|
||||
if let Some(key) = lfu {
|
||||
self.map.remove(&key);
|
||||
fn recency_order<K, V, S>(cache: &LfuCache<K, V, S>) -> Vec<EntryData<K, V>> {
|
||||
let mut entries = vec![];
|
||||
for item in cache.recency_index.iter() {
|
||||
entries.push(EntryData::new(item));
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decay() {
|
||||
let mut cache = LfuCacheU64::with_capacity(4);
|
||||
for i in 0..4 {
|
||||
cache.put(i, i);
|
||||
for _ in 0..i * 2 {
|
||||
cache.get(&i);
|
||||
}
|
||||
}
|
||||
self.latest.replace(id);
|
||||
self.map.insert(id, ValueWithFreq { value: v, freq: 0 });
|
||||
prior.map(|ent| ent.value)
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 1,
|
||||
key: 0,
|
||||
value: 0,
|
||||
},
|
||||
EntryData {
|
||||
freq: 2,
|
||||
last_tick: 4,
|
||||
key: 1,
|
||||
value: 1,
|
||||
},
|
||||
EntryData {
|
||||
freq: 4,
|
||||
last_tick: 9,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 16,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
|
||||
cache.get(&1);
|
||||
cache.get(&2);
|
||||
cache.put(10, 10);
|
||||
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 19,
|
||||
key: 10,
|
||||
value: 10,
|
||||
},
|
||||
EntryData {
|
||||
freq: 3,
|
||||
last_tick: 17,
|
||||
key: 1,
|
||||
value: 1,
|
||||
},
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 18,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 16,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
|
||||
cache.get(&10);
|
||||
cache.put(11, 11);
|
||||
// bump up freq of 11 so that we can displace 1 on the next put
|
||||
cache.get(&11);
|
||||
cache.get(&11);
|
||||
cache.get(&11);
|
||||
cache.get(&11);
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 3,
|
||||
last_tick: 17,
|
||||
key: 1,
|
||||
value: 1,
|
||||
},
|
||||
EntryData {
|
||||
freq: 4,
|
||||
last_tick: 25,
|
||||
key: 11,
|
||||
value: 11,
|
||||
},
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 18,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 16,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
|
||||
cache.put(12, 12);
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 26,
|
||||
key: 12,
|
||||
value: 12,
|
||||
},
|
||||
EntryData {
|
||||
freq: 4,
|
||||
last_tick: 25,
|
||||
key: 11,
|
||||
value: 11,
|
||||
},
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 18,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 16,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
|
||||
// Ensure that we're all non-zero
|
||||
for _ in 0..5 {
|
||||
cache.get(&2);
|
||||
cache.get(&11);
|
||||
cache.get(&12);
|
||||
}
|
||||
|
||||
// and bump up the ticks so that we trigger decay for 3
|
||||
for _ in 0..10 {
|
||||
cache.get(&11);
|
||||
}
|
||||
|
||||
// Note that key: 3 has freq 6 in this snapshot
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 41,
|
||||
key: 12,
|
||||
value: 12,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 16,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
EntryData {
|
||||
freq: 10,
|
||||
last_tick: 39,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 19,
|
||||
last_tick: 51,
|
||||
key: 11,
|
||||
value: 11,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
|
||||
// trigger an eviction. This will decay key 3's freq
|
||||
// and it will be evicted, even though key 12 in
|
||||
// the snapshot above had freq 5 when key 3 had freq 6.
|
||||
cache.put(42, 42);
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 52,
|
||||
key: 42,
|
||||
value: 42,
|
||||
},
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 41,
|
||||
key: 12,
|
||||
value: 12,
|
||||
},
|
||||
EntryData {
|
||||
freq: 10,
|
||||
last_tick: 39,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 19,
|
||||
last_tick: 51,
|
||||
key: 11,
|
||||
value: 11,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviction() {
|
||||
let mut cache = LfuCacheU64::with_capacity(8);
|
||||
for i in 0..8 {
|
||||
cache.put(i, i);
|
||||
for _ in 0..i {
|
||||
cache.get(&i);
|
||||
}
|
||||
}
|
||||
|
||||
k9::assert_equal!(cache.len(), 8);
|
||||
cache.put(8, 8);
|
||||
k9::assert_equal!(cache.len(), 8);
|
||||
|
||||
let freq = frequency_order(&cache);
|
||||
k9::assert_equal!(*freq[0].key, 8, "0 got evicted, so 8 is first");
|
||||
k9::snapshot!(
|
||||
freq,
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 37,
|
||||
key: 8,
|
||||
value: 8,
|
||||
},
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 3,
|
||||
key: 1,
|
||||
value: 1,
|
||||
},
|
||||
EntryData {
|
||||
freq: 2,
|
||||
last_tick: 6,
|
||||
key: 2,
|
||||
value: 2,
|
||||
},
|
||||
EntryData {
|
||||
freq: 3,
|
||||
last_tick: 10,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
EntryData {
|
||||
freq: 4,
|
||||
last_tick: 15,
|
||||
key: 4,
|
||||
value: 4,
|
||||
},
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 21,
|
||||
key: 5,
|
||||
value: 5,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 28,
|
||||
key: 6,
|
||||
value: 6,
|
||||
},
|
||||
EntryData {
|
||||
freq: 7,
|
||||
last_tick: 36,
|
||||
key: 7,
|
||||
value: 7,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
|
||||
for i in 9..12 {
|
||||
cache.put(i, i);
|
||||
cache.get(&i);
|
||||
}
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
"
|
||||
[
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 39,
|
||||
key: 9,
|
||||
value: 9,
|
||||
},
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 41,
|
||||
key: 10,
|
||||
value: 10,
|
||||
},
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 10,
|
||||
key: 3,
|
||||
value: 3,
|
||||
},
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 43,
|
||||
key: 11,
|
||||
value: 11,
|
||||
},
|
||||
EntryData {
|
||||
freq: 4,
|
||||
last_tick: 15,
|
||||
key: 4,
|
||||
value: 4,
|
||||
},
|
||||
EntryData {
|
||||
freq: 5,
|
||||
last_tick: 21,
|
||||
key: 5,
|
||||
value: 5,
|
||||
},
|
||||
EntryData {
|
||||
freq: 6,
|
||||
last_tick: 28,
|
||||
key: 6,
|
||||
value: 6,
|
||||
},
|
||||
EntryData {
|
||||
freq: 7,
|
||||
last_tick: 36,
|
||||
key: 7,
|
||||
value: 7,
|
||||
},
|
||||
]
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let mut cache = LfuCacheU64::<&'static str>::with_capacity(8);
|
||||
cache.put(1, "hello");
|
||||
cache.put(2, "there");
|
||||
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
r#"
|
||||
[
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 1,
|
||||
key: 1,
|
||||
value: "hello",
|
||||
},
|
||||
EntryData {
|
||||
freq: 0,
|
||||
last_tick: 2,
|
||||
key: 2,
|
||||
value: "there",
|
||||
},
|
||||
]
|
||||
"#
|
||||
);
|
||||
|
||||
cache.get(&1);
|
||||
cache.get(&1);
|
||||
cache.get(&1);
|
||||
cache.get(&2);
|
||||
|
||||
k9::snapshot!(
|
||||
frequency_order(&cache),
|
||||
r#"
|
||||
[
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 6,
|
||||
key: 2,
|
||||
value: "there",
|
||||
},
|
||||
EntryData {
|
||||
freq: 3,
|
||||
last_tick: 5,
|
||||
key: 1,
|
||||
value: "hello",
|
||||
},
|
||||
]
|
||||
"#
|
||||
);
|
||||
|
||||
k9::snapshot!(
|
||||
recency_order(&cache),
|
||||
r#"
|
||||
[
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 6,
|
||||
key: 2,
|
||||
value: "there",
|
||||
},
|
||||
EntryData {
|
||||
freq: 3,
|
||||
last_tick: 5,
|
||||
key: 1,
|
||||
value: "hello",
|
||||
},
|
||||
]
|
||||
"#
|
||||
);
|
||||
|
||||
cache.get(&1);
|
||||
k9::snapshot!(
|
||||
recency_order(&cache),
|
||||
r#"
|
||||
[
|
||||
EntryData {
|
||||
freq: 4,
|
||||
last_tick: 7,
|
||||
key: 1,
|
||||
value: "hello",
|
||||
},
|
||||
EntryData {
|
||||
freq: 1,
|
||||
last_tick: 6,
|
||||
key: 2,
|
||||
value: "there",
|
||||
},
|
||||
]
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,4 +12,4 @@ proc-macro = true
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0.2"
|
||||
syn = "1.0"
|
||||
syn = {version="1.0", features=["extra-traits"]}
|
||||
|
@ -22,6 +22,7 @@ finl_unicode = "1.2"
|
||||
freetype = { path = "../deps/freetype" }
|
||||
harfbuzz = { path = "../deps/harfbuzz" }
|
||||
lazy_static = "1.4"
|
||||
lfucache = { path = "../lfucache" }
|
||||
log = "0.4"
|
||||
memmap2 = "0.2"
|
||||
metrics = { version="0.17", features=["std"]}
|
||||
|
@ -661,8 +661,7 @@ impl FontShaper for HarfbuzzShaper {
|
||||
let range = range.unwrap_or_else(|| 0..text.len());
|
||||
|
||||
log::trace!(
|
||||
"shape byte_len={} `{}` with presentation={presentation:?}",
|
||||
text.len(),
|
||||
"shape {range:?} `{}` with presentation={presentation:?}",
|
||||
text.escape_debug()
|
||||
);
|
||||
let start = std::time::Instant::now();
|
||||
|
@ -12,6 +12,7 @@ use config::{AllowSquareGlyphOverflow, TextStyle};
|
||||
use euclid::num::Zero;
|
||||
use lfucache::LfuCacheU64;
|
||||
use ordered_float::NotNan;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::rc::Rc;
|
||||
@ -198,8 +199,8 @@ impl<'a> BitmapImage for DecodedImageHandle<'a> {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DecodedImage {
|
||||
frame_start: Instant,
|
||||
current_frame: usize,
|
||||
frame_start: RefCell<Instant>,
|
||||
current_frame: RefCell<usize>,
|
||||
image: Arc<ImageData>,
|
||||
}
|
||||
|
||||
@ -208,8 +209,8 @@ impl DecodedImage {
|
||||
// A single black pixel
|
||||
let image = ImageData::with_data(ImageDataType::new_single_frame(1, 1, vec![0, 0, 0, 0]));
|
||||
Self {
|
||||
frame_start: Instant::now(),
|
||||
current_frame: 0,
|
||||
frame_start: RefCell::new(Instant::now()),
|
||||
current_frame: RefCell::new(0),
|
||||
image: Arc::new(image),
|
||||
}
|
||||
}
|
||||
@ -228,15 +229,15 @@ impl DecodedImage {
|
||||
0
|
||||
};
|
||||
Self {
|
||||
frame_start: Instant::now(),
|
||||
current_frame,
|
||||
frame_start: RefCell::new(Instant::now()),
|
||||
current_frame: RefCell::new(current_frame),
|
||||
image: Arc::clone(image_data),
|
||||
}
|
||||
}
|
||||
|
||||
_ => Self {
|
||||
frame_start: Instant::now(),
|
||||
current_frame: 0,
|
||||
frame_start: RefCell::new(Instant::now()),
|
||||
current_frame: RefCell::new(0),
|
||||
image: Arc::clone(image_data),
|
||||
},
|
||||
}
|
||||
@ -582,12 +583,12 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
fn cached_image_impl(
|
||||
frame_cache: &mut HashMap<[u8; 32], Sprite<T>>,
|
||||
atlas: &mut Atlas<T>,
|
||||
decoded: &mut DecodedImage,
|
||||
decoded: &DecodedImage,
|
||||
padding: Option<usize>,
|
||||
) -> anyhow::Result<(Sprite<T>, Option<Instant>)> {
|
||||
let mut handle = DecodedImageHandle {
|
||||
h: decoded.image.data(),
|
||||
current_frame: decoded.current_frame,
|
||||
current_frame: *decoded.current_frame.borrow(),
|
||||
};
|
||||
match &*handle.h {
|
||||
ImageDataType::Rgba8 { hash, .. } => {
|
||||
@ -606,28 +607,31 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
..
|
||||
} => {
|
||||
let mut next = None;
|
||||
let mut decoded_frame_start = decoded.frame_start.borrow_mut();
|
||||
let mut decoded_current_frame = decoded.current_frame.borrow_mut();
|
||||
if frames.len() > 1 {
|
||||
let now = Instant::now();
|
||||
let mut next_due = decoded.frame_start + durations[decoded.current_frame];
|
||||
|
||||
let mut next_due = *decoded_frame_start + durations[*decoded_current_frame];
|
||||
if now >= next_due {
|
||||
// Advance to next frame
|
||||
decoded.current_frame += 1;
|
||||
if decoded.current_frame >= frames.len() {
|
||||
decoded.current_frame = 0;
|
||||
*decoded_current_frame = *decoded_current_frame + 1;
|
||||
if *decoded_current_frame >= frames.len() {
|
||||
*decoded_current_frame = 0;
|
||||
// Skip potential 0-duration root frame
|
||||
if durations[0].as_millis() == 0 && frames.len() > 1 {
|
||||
decoded.current_frame += 1;
|
||||
*decoded_current_frame = *decoded_current_frame + 1;
|
||||
}
|
||||
}
|
||||
decoded.frame_start = now;
|
||||
next_due = decoded.frame_start + durations[decoded.current_frame];
|
||||
handle.current_frame = decoded.current_frame;
|
||||
*decoded_frame_start = now;
|
||||
next_due = *decoded_frame_start + durations[*decoded_current_frame];
|
||||
handle.current_frame = *decoded_current_frame;
|
||||
}
|
||||
|
||||
next.replace(next_due);
|
||||
}
|
||||
|
||||
let hash = hashes[decoded.current_frame];
|
||||
let hash = hashes[*decoded_current_frame];
|
||||
|
||||
if let Some(sprite) = frame_cache.get(&hash) {
|
||||
return Ok((sprite.clone(), next));
|
||||
@ -639,7 +643,7 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
|
||||
return Ok((
|
||||
sprite,
|
||||
Some(decoded.frame_start + durations[decoded.current_frame]),
|
||||
Some(*decoded_frame_start + durations[*decoded_current_frame]),
|
||||
));
|
||||
}
|
||||
ImageDataType::EncodedFile(_) => unreachable!(),
|
||||
@ -653,16 +657,12 @@ impl<T: Texture2d> GlyphCache<T> {
|
||||
) -> anyhow::Result<(Sprite<T>, Option<Instant>)> {
|
||||
let id = image_data.id() as u64;
|
||||
|
||||
if let Some(decoded) = self.image_cache.get_mut(&id) {
|
||||
if let Some(decoded) = self.image_cache.get(&id) {
|
||||
Self::cached_image_impl(&mut self.frame_cache, &mut self.atlas, decoded, padding)
|
||||
} else {
|
||||
let mut decoded = DecodedImage::load(image_data);
|
||||
let res = Self::cached_image_impl(
|
||||
&mut self.frame_cache,
|
||||
&mut self.atlas,
|
||||
&mut decoded,
|
||||
padding,
|
||||
)?;
|
||||
let decoded = DecodedImage::load(image_data);
|
||||
let res =
|
||||
Self::cached_image_impl(&mut self.frame_cache, &mut self.atlas, &decoded, padding)?;
|
||||
self.image_cache.put(id, decoded);
|
||||
Ok(res)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use std::rc::Rc;
|
||||
use wezterm_font::shaper::GlyphInfo;
|
||||
use wezterm_font::units::*;
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub struct ShapeCacheKey {
|
||||
pub style: TextStyle,
|
||||
pub text: String,
|
||||
@ -82,7 +82,7 @@ impl<'a> BorrowedShapeCacheKey<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ShapeCacheKeyTrait {
|
||||
pub trait ShapeCacheKeyTrait: std::fmt::Debug {
|
||||
fn key<'k>(&'k self) -> BorrowedShapeCacheKey<'k>;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user