diff --git a/src/export/render.rs b/src/export/render.rs index 50257e1c2..34fb43317 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -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; diff --git a/src/lib.rs b/src/lib.rs index 06324c115..5173b0221 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, @@ -86,8 +84,6 @@ pub struct Context { styles: Arc, /// Cached modules. modules: HashMap, - /// Cached queries. - cache: HashMap, /// The stack of imported files that led to evaluation of the current file. route: Vec, /// 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, - /// 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( - &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) - } -} diff --git a/src/model/layout.rs b/src/model/layout.rs index 3b82ddc28..1933fca10 100644 --- a/src/model/layout.rs +++ b/src/model/layout.rs @@ -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>> { - 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 { diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 000000000..97af01399 --- /dev/null +++ b/src/query.rs @@ -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 = RefCell::default(); +} + +/// A map from hashes to cache entries. +type Cache = HashMap; + +/// Access the cache. +fn with(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, + /// 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(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(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(&self, _: &mut H) {} + } + }; +} + +skip!(crate::font::FontStore); +skip!(crate::Context);