Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve the performance of trimming shape run cache #298

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,8 @@ type BuildHasher = core::hash::BuildHasherDefault<rustc_hash::FxHasher>;
type HashMap<K, V> = std::collections::HashMap<K, V, BuildHasher>;
#[cfg(not(feature = "std"))]
type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasher>;

#[cfg(feature = "std")]
type HashSet<V> = std::collections::HashSet<V, BuildHasher>;
#[cfg(not(feature = "std"))]
type HashSet<V> = hashbrown::HashSet<V, BuildHasher>;
118 changes: 113 additions & 5 deletions src/shape_run_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use alloc::{string::String, vec::Vec};
use core::ops::Range;

use crate::{AttrsOwned, HashMap, ShapeGlyph};
use crate::{AttrsOwned, HashMap, HashSet, ShapeGlyph};

/// Key for caching shape runs.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
Expand All @@ -13,32 +13,72 @@ pub struct ShapeRunKey {
}

/// A helper structure for caching shape runs.
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct ShapeRunCache {
age: u64,
cache: HashMap<ShapeRunKey, (u64, Vec<ShapeGlyph>)>,
age_registries: Vec<HashSet<ShapeRunKey>>,
}

impl Default for ShapeRunCache {
#[allow(clippy::vec_init_then_push)]
fn default() -> Self {
let mut age_registries = Vec::new();
age_registries.push(HashSet::default());

Self {
age: 0,
cache: Default::default(),
age_registries,
jackpot51 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

impl ShapeRunCache {
/// Get cache item, updating age if found
pub fn get(&mut self, key: &ShapeRunKey) -> Option<&Vec<ShapeGlyph>> {
self.cache.get_mut(key).map(|(age, glyphs)| {
*age = self.age;
if *age != self.age {
// remove the key from the old age registry
let index = (self.age - *age) as usize;
self.age_registries[index].remove(key);

// update age
*age = self.age;
// register the key to the new age registry
if let Some(keys) = self.age_registries.first_mut() {
keys.insert(key.clone());
}
Comment on lines +43 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.age_registries[index].remove(key);
// update age
*age = self.age;
// register the key to the new age registry
if let Some(keys) = self.age_registries.first_mut() {
keys.insert(key.clone());
}
let prev_copy = self.age_registries[index].take(key);
// update age
*age = self.age;
// register the key to the new age registry
if let Some(keys) = self.age_registries.first_mut() {
// Note: This is only valid so long as the PartialEq impl on ShapeRunKey checks value
// equality.
keys.insert(prev_copy.expect("age_registries should have entry if cache has entry"));
}

}
&*glyphs
})
}

/// Insert cache item with current age
pub fn insert(&mut self, key: ShapeRunKey, glyphs: Vec<ShapeGlyph>) {
if let Some(keys) = self.age_registries.first_mut() {
// register the key to the current age
keys.insert(key.clone());
}
self.cache.insert(key, (self.age, glyphs));
}

/// Remove anything in the cache with an age older than keep_ages
pub fn trim(&mut self, keep_ages: u64) {
self.cache
.retain(|_key, (age, _glyphs)| *age + keep_ages >= self.age);
// remove the age registries that's greater than kept ages
// and remove the keys from cache saved in the registries
while self.age_registries.len() as u64 > keep_ages {
if let Some(keys) = self.age_registries.pop() {
for key in keys {
self.cache.remove(&key);
}
}
}
Comment on lines +68 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// and remove the keys from cache saved in the registries
while self.age_registries.len() as u64 > keep_ages {
if let Some(keys) = self.age_registries.pop() {
for key in keys {
self.cache.remove(&key);
}
}
}
// and remove the keys from cache saved in the registries
let mut recovered_keys: Option<HashSet<ShapeRunKey>> = None;
while self.age_registries.len() as u64 > keep_ages {
if let Some(keys) = self.age_registries.pop() {
for key in keys.drain() {
self.cache.remove(&key);
}
if recovered_keys.is_none() {
recovered_keys = Some(keys);
}
}
}

// Increase age
self.age += 1;
// insert a new registry to the front of the Vec
// to keep keys for the current age
self.age_registries.insert(0, HashSet::default());
Comment on lines +78 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can potentially reuse a hashset popped earlier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.age_registries.insert(0, HashSet::default());
self.age_registries.insert(0, recovered_keys.unwrap_or_default());

}
}

Expand All @@ -47,3 +87,71 @@ impl core::fmt::Debug for ShapeRunCache {
f.debug_tuple("ShapeRunCache").finish()
}
}

#[cfg(test)]
mod test {
use crate::{Attrs, AttrsOwned, ShapeRunCache, ShapeRunKey};

#[test]
fn test_trim() {
let key1 = ShapeRunKey {
text: "1".to_string(),
default_attrs: AttrsOwned::new(Attrs::new()),
attrs_spans: Vec::new(),
};

let key2 = ShapeRunKey {
text: "2".to_string(),
default_attrs: AttrsOwned::new(Attrs::new()),
attrs_spans: Vec::new(),
};

let key3 = ShapeRunKey {
text: "3".to_string(),
default_attrs: AttrsOwned::new(Attrs::new()),
attrs_spans: Vec::new(),
};

let mut cache = ShapeRunCache::default();

cache.insert(key1.clone(), Vec::new());
cache.insert(key2.clone(), Vec::new());
cache.insert(key3.clone(), Vec::new());
// this will trim everything
cache.trim(0);
assert!(cache.cache.is_empty());

cache.insert(key1.clone(), Vec::new());
cache.insert(key2.clone(), Vec::new());
cache.insert(key3.clone(), Vec::new());
// keep 1 age
cache.trim(1);
// all was just inserted so all kept
assert_eq!(cache.cache.len(), 3);
assert_eq!(cache.age_registries.len(), 2);

cache.get(&key1);
cache.get(&key2);
cache.trim(1);
// only key1 and key2 was refreshed, so key3 was trimed
assert_eq!(cache.cache.len(), 2);
assert_eq!(cache.age_registries.len(), 2);

cache.get(&key1);
cache.trim(1);
// only key1 was refreshed, so key2 was trimed
assert_eq!(cache.cache.len(), 1);
assert_eq!(cache.age_registries.len(), 2);

cache.trim(2);
// keep 2 ages, so even key1 wasn't refreshed,
// it was still kept
assert_eq!(cache.cache.len(), 1);
assert_eq!(cache.age_registries.len(), 3);

cache.trim(2);
// key1 is now too old for 2 ages, so it was trimed
assert_eq!(cache.cache.len(), 0);
assert_eq!(cache.age_registries.len(), 3);
}
}
Loading