1
1
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:
Wez Furlong 2022-11-14 07:34:38 -07:00
parent 72757b8849
commit 0516b61f62
No known key found for this signature in database
8 changed files with 829 additions and 193 deletions

52
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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",
},
]
"#
);
}
}

View File

@ -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"]}

View File

@ -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"]}

View File

@ -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();

View File

@ -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)
}

View File

@ -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>;
}