Thread-local query cache

This commit is contained in:
Laurenz 2022-05-25 10:33:20 +02:00
parent 018860da9c
commit 362a7f2a8a
4 changed files with 138 additions and 87 deletions

View File

@ -12,6 +12,7 @@ use crate::geom::{
self, Geometry, Length, Paint, PathElement, Shape, Size, Stroke, Transform,
};
use crate::image::{Image, RasterImage, Svg};
use crate::query::query_ref;
use crate::Context;
/// Export a frame into a rendered image.
@ -240,17 +241,15 @@ fn render_outline_glyph(
return Some(());
}
// TODO(query)
// Rasterize the glyph with `pixglyph`.
// Try to retrieve a prepared glyph or prepare it from scratch if it
// doesn't exist, yet.
let glyph = ctx
.query((text.face_id, id), |ctx, (face_id, id)| {
pixglyph::Glyph::load(ctx.fonts.get(face_id).ttf(), id)
})
.as_ref()?;
let bitmap = query_ref(
(&ctx.fonts, text.face_id, id),
|(fonts, face_id, id)| pixglyph::Glyph::load(fonts.get(face_id).ttf(), id),
|glyph| glyph.as_ref().map(|g| g.rasterize(ts.tx, ts.ty, ppem)),
)?;
// Rasterize the glyph with `pixglyph`.
let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem);
let cw = canvas.width() as i32;
let ch = canvas.height() as i32;
let mw = bitmap.width as i32;

View File

@ -47,13 +47,11 @@ pub mod library;
pub mod loading;
pub mod model;
pub mod parse;
pub mod query;
pub mod source;
pub mod syntax;
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::hash::Hash;
use std::mem;
use std::path::PathBuf;
use std::sync::Arc;
@ -68,7 +66,7 @@ use crate::model::StyleMap;
use crate::source::{SourceId, SourceStore};
use crate::util::PathExt;
/// The core context which holds the loader, configuration and cached artifacts.
/// The core context which holds the loader, stores, and configuration.
pub struct Context {
/// The loader the context was created with.
pub loader: Arc<dyn Loader>,
@ -86,8 +84,6 @@ pub struct Context {
styles: Arc<StyleMap>,
/// Cached modules.
modules: HashMap<SourceId, Module>,
/// Cached queries.
cache: HashMap<u64, CacheEntry>,
/// The stack of imported files that led to evaluation of the current file.
route: Vec<SourceId>,
/// The dependencies of the current evaluation process.
@ -236,77 +232,9 @@ impl ContextBuilder {
std: self.std.clone().unwrap_or_else(|| Arc::new(library::new())),
styles: self.styles.clone().unwrap_or_default(),
modules: HashMap::new(),
cache: HashMap::new(),
route: vec![],
deps: vec![],
flow: None,
}
}
}
/// An entry in the query cache.
struct CacheEntry {
/// The query's results.
data: Box<dyn Any>,
/// How many evictions have passed since the entry has been last used.
age: usize,
}
impl Context {
/// Execute a query.
///
/// This hashes all inputs to the query and then either returns a cached
/// version or executes the query, saves the results in the cache and
/// returns a reference to them.
pub fn query<I, O>(
&mut self,
input: I,
query: fn(ctx: &mut Self, input: I) -> O,
) -> &O
where
I: Hash,
O: 'static,
{
let hash = fxhash::hash64(&input);
if !self.cache.contains_key(&hash) {
let output = query(self, input);
self.cache.insert(hash, CacheEntry { data: Box::new(output), age: 0 });
}
let entry = self.cache.get_mut(&hash).unwrap();
entry.age = 0;
entry.data.downcast_ref().expect("oh no, a hash collision")
}
/// Garbage-collect the query cache. This deletes elements which haven't
/// been used in a while.
///
/// Returns details about the eviction.
pub fn evict(&mut self) -> Eviction {
const MAX_AGE: usize = 5;
let before = self.cache.len();
self.cache.retain(|_, entry| {
entry.age += 1;
entry.age <= MAX_AGE
});
Eviction { before, after: self.cache.len() }
}
}
/// Details about a cache eviction.
pub struct Eviction {
/// The number of items in the cache before the eviction.
pub before: usize,
/// The number of items in the cache after the eviction.
pub after: usize,
}
impl Display for Eviction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
writeln!(f, "Before: {}", self.before)?;
writeln!(f, "Evicted: {}", self.before - self.after)?;
writeln!(f, "After: {}", self.after)
}
}

View File

@ -14,6 +14,7 @@ use crate::geom::{
};
use crate::library::graphics::MoveNode;
use crate::library::layout::{AlignNode, PadNode};
use crate::query::query;
use crate::util::Prehashed;
use crate::Context;
@ -221,11 +222,13 @@ impl Layout for LayoutNode {
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> {
ctx.query((self, regions, styles), |ctx, (node, regions, styles)| {
let entry = StyleEntry::Barrier(Barrier::new(node.id()));
node.0.layout(ctx, regions, entry.chain(&styles))
})
.clone()
query(
(self, ctx, regions, styles),
|(node, ctx, regions, styles)| {
let entry = StyleEntry::Barrier(Barrier::new(node.id()));
node.0.layout(ctx, regions, entry.chain(&styles))
},
)
}
fn pack(self) -> LayoutNode {

121
src/query.rs Normal file
View File

@ -0,0 +1,121 @@
//! Query caching.
use std::any::Any;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::hash::Hash;
thread_local! {
/// The thread-local query cache.
static CACHE: RefCell<Cache> = RefCell::default();
}
/// A map from hashes to cache entries.
type Cache = HashMap<u64, CacheEntry>;
/// Access the cache.
fn with<F, R>(f: F) -> R
where
F: FnOnce(&mut Cache) -> R,
{
CACHE.with(|cell| f(&mut cell.borrow_mut()))
}
/// An entry in the query cache.
struct CacheEntry {
/// The query's results.
data: Box<dyn Any>,
/// How many evictions have passed since the entry has been last used.
age: usize,
}
/// Execute a query.
///
/// This hashes all inputs to the query and then either returns a cached version
/// from the thread-local query cache or executes the query and saves a copy of
/// the results in the cache.
///
/// Note that `f` must be a pure function.
pub fn query<I, O>(input: I, f: fn(input: I) -> O) -> O
where
I: Hash,
O: Clone + 'static,
{
query_ref(input, f, Clone::clone)
}
/// Execute a query and then call a function with a reference to the result.
///
/// This hashes all inputs to the query and then either call `g` with a cached
/// version from the thread-local query cache or executes the query, calls `g`
/// with the fresh version and saves the result in the cache.
///
/// Note that `f` must be a pure function, while `g` does not need to be pure.
pub fn query_ref<I, O, G, R>(input: I, f: fn(input: I) -> O, g: G) -> R
where
I: Hash,
O: 'static,
G: Fn(&O) -> R,
{
let hash = fxhash::hash64(&input);
let result = with(|cache| {
let entry = cache.get_mut(&hash)?;
entry.age = 0;
entry.data.downcast_ref().map(|output| g(output))
});
result.unwrap_or_else(|| {
let output = f(input);
let result = g(&output);
let entry = CacheEntry { data: Box::new(output), age: 0 };
with(|cache| cache.insert(hash, entry));
result
})
}
/// Garbage-collect the thread-local query cache.
///
/// This deletes elements which haven't been used in a while and returns details
/// about the eviction.
pub fn evict() -> Eviction {
with(|cache| {
const MAX_AGE: usize = 5;
let before = cache.len();
cache.retain(|_, entry| {
entry.age += 1;
entry.age <= MAX_AGE
});
Eviction { before, after: cache.len() }
})
}
/// Details about a cache eviction.
pub struct Eviction {
/// The number of items in the cache before the eviction.
pub before: usize,
/// The number of items in the cache after the eviction.
pub after: usize,
}
impl Display for Eviction {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
writeln!(f, "Before: {}", self.before)?;
writeln!(f, "Evicted: {}", self.before - self.after)?;
writeln!(f, "After: {}", self.after)
}
}
// These impls are temporary and incorrect.
macro_rules! skip {
($ty:ty) => {
impl Hash for $ty {
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
}
};
}
skip!(crate::font::FontStore);
skip!(crate::Context);