diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 43fec7d6b..7c00dab46 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -36,6 +36,9 @@ jobs: toolchain: ${{ matrix.rust }} override: true + - name: Dependency cache + uses: Swatinem/rust-cache@v1 + - name: Build uses: actions-rs/cargo@v1 with: @@ -47,3 +50,9 @@ jobs: with: command: test args: --manifest-path typst/Cargo.toml --all-features + + - name: Test without incremental + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path typst/Cargo.toml --no-default-features --features fs diff --git a/Cargo.toml b/Cargo.toml index b18b0af56..2cb23c4e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,10 @@ authors = ["The Typst Project Developers"] edition = "2018" [features] -default = ["cli", "fs"] +default = ["cli", "fs", "layout-cache"] cli = ["anyhow", "fs", "same-file"] fs = ["dirs", "memmap2", "same-file", "walkdir"] +layout-cache = [] [workspace] members = ["bench"] diff --git a/bench/Cargo.toml b/bench/Cargo.toml index 4965c3ea4..260a2ba14 100644 --- a/bench/Cargo.toml +++ b/bench/Cargo.toml @@ -5,9 +5,13 @@ authors = ["The Typst Project Developers"] edition = "2018" publish = false +[features] +default = ["layout-cache"] +layout-cache = ["typst/layout-cache"] + [dev-dependencies] -criterion = "0.3" -typst = { path = ".." } +criterion = { version = "0.3", features = ["html_reports"] } +typst = { path = "..", default-features = false, features = ["fs"] } [[bench]] name = "typst" diff --git a/bench/src/bench.rs b/bench/src/bench.rs index 832d538f2..c86ccc899 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -1,14 +1,17 @@ +use std::cell::RefCell; use std::path::Path; use std::rc::Rc; use criterion::{criterion_group, criterion_main, Criterion}; -use typst::eval::eval; -use typst::exec::exec; +use typst::cache::Cache; +use typst::eval::{eval, Module, Scope}; +use typst::exec::{exec, State}; use typst::export::pdf; -use typst::layout::layout; +use typst::layout::{self, layout, Frame}; use typst::loading::FsLoader; use typst::parse::parse; +use typst::syntax; use typst::typeset; const FONT_DIR: &str = "../fonts"; @@ -16,42 +19,144 @@ const TYP_DIR: &str = "../tests/typ"; const CASES: &[&str] = &["coma.typ", "text/basic.typ"]; fn benchmarks(c: &mut Criterion) { - let mut loader = FsLoader::new(); - loader.search_path(FONT_DIR); - - let mut cache = typst::cache::Cache::new(&loader); - let scope = typst::library::new(); - let state = typst::exec::State::default(); - + let ctx = Context::new(); for case in CASES { let path = Path::new(TYP_DIR).join(case); let name = path.file_stem().unwrap().to_string_lossy(); + let src = std::fs::read_to_string(&path).unwrap(); + let case = Case::new(src, ctx.clone()); + /// Bench with all caches. macro_rules! bench { - ($step:literal: $code:expr) => { + ($step:literal, setup = |$cache:ident| $setup:expr, code = $code:expr $(,)?) => { c.bench_function(&format!("{}-{}", $step, name), |b| { - b.iter(|| { - cache.layout.clear(); - $code - }); + b.iter_batched( + || { + let mut borrowed = ctx.borrow_mut(); + let $cache = &mut borrowed.cache; + $setup + }, + |_| $code, + criterion::BatchSize::PerIteration, + ) }); }; + ($step:literal, $code:expr) => { + c.bench_function(&format!("{}-{}", $step, name), |b| b.iter(|| $code)); + }; } - // Prepare intermediate results, run warm and fill caches. - let src = std::fs::read_to_string(&path).unwrap(); - let tree = Rc::new(parse(&src).output); - let evaluated = eval(&mut loader, &mut cache, Some(&path), tree.clone(), &scope); - let executed = exec(&evaluated.output.template, state.clone()); - let layouted = layout(&mut loader, &mut cache, &executed.output); + bench!("parse", case.parse()); + bench!("eval", case.eval()); + bench!("exec", case.exec()); - // Bench! - bench!("parse": parse(&src)); - bench!("eval": eval(&mut loader, &mut cache, Some(&path), tree.clone(), &scope)); - bench!("exec": exec(&evaluated.output.template, state.clone())); - bench!("layout": layout(&mut loader, &mut cache, &executed.output)); - bench!("typeset": typeset(&mut loader, &mut cache, Some(&path), &src, &scope, state.clone())); - bench!("pdf": pdf(&cache, &layouted)); + #[cfg(not(feature = "layout-cache"))] + { + bench!("layout", case.layout()); + bench!("typeset", case.typeset()); + } + + #[cfg(feature = "layout-cache")] + { + bench!( + "layout", + setup = |cache| cache.layout.clear(), + code = case.layout(), + ); + bench!( + "typeset", + setup = |cache| cache.layout.clear(), + code = case.typeset(), + ); + bench!("layout-cached", case.layout()); + bench!("typeset-cached", case.typeset()); + } + + bench!("pdf", case.pdf()); + } +} + +/// The context required for benchmarking a case. +struct Context { + loader: FsLoader, + cache: Cache, +} + +impl Context { + fn new() -> Rc> { + let mut loader = FsLoader::new(); + loader.search_path(FONT_DIR); + let cache = Cache::new(&loader); + Rc::new(RefCell::new(Self { loader, cache })) + } +} + +/// A test case with prepared intermediate results. +struct Case { + ctx: Rc>, + src: String, + scope: Scope, + state: State, + ast: Rc, + module: Module, + tree: layout::Tree, + frames: Vec>, +} + +impl Case { + fn new(src: impl Into, ctx: Rc>) -> Self { + let mut borrowed = ctx.borrow_mut(); + let Context { loader, cache } = &mut *borrowed; + let scope = typst::library::new(); + let state = typst::exec::State::default(); + let src = src.into(); + let ast = Rc::new(parse(&src).output); + let module = eval(loader, cache, None, ast.clone(), &scope).output; + let tree = exec(&module.template, state.clone()).output; + let frames = layout(loader, cache, &tree); + drop(borrowed); + Self { + ctx, + src, + scope, + state, + ast, + module, + tree, + frames, + } + } + + fn parse(&self) -> syntax::Tree { + parse(&self.src).output + } + + fn eval(&self) -> Module { + let mut borrowed = self.ctx.borrow_mut(); + let Context { loader, cache } = &mut *borrowed; + eval(loader, cache, None, self.ast.clone(), &self.scope).output + } + + fn exec(&self) -> layout::Tree { + exec(&self.module.template, self.state.clone()).output + } + + fn layout(&self) -> Vec> { + let mut borrowed = self.ctx.borrow_mut(); + let Context { loader, cache } = &mut *borrowed; + layout(loader, cache, &self.tree) + } + + fn typeset(&self) -> Vec> { + let mut borrowed = self.ctx.borrow_mut(); + let Context { loader, cache } = &mut *borrowed; + let state = self.state.clone(); + typeset(loader, cache, None, &self.src, &self.scope, state).output + } + + fn pdf(&self) -> Vec { + let ctx = self.ctx.borrow(); + pdf(&ctx.cache, &self.frames) } } diff --git a/src/cache.rs b/src/cache.rs index aa9c10a01..2aa276aa4 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -2,6 +2,7 @@ use crate::font::FontCache; use crate::image::ImageCache; +#[cfg(feature = "layout-cache")] use crate::layout::LayoutCache; use crate::loading::Loader; @@ -12,6 +13,7 @@ pub struct Cache { /// Caches decoded images. pub image: ImageCache, /// Caches layouting artifacts. + #[cfg(feature = "layout-cache")] pub layout: LayoutCache, } @@ -21,6 +23,7 @@ impl Cache { Self { font: FontCache::new(loader), image: ImageCache::new(), + #[cfg(feature = "layout-cache")] layout: LayoutCache::new(), } } diff --git a/src/layout/background.rs b/src/layout/background.rs index 8390a7568..013a887a8 100644 --- a/src/layout/background.rs +++ b/src/layout/background.rs @@ -1,7 +1,8 @@ use super::*; /// A node that places a rectangular filled background behind its child. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct BackgroundNode { /// The kind of shape to use as a background. pub shape: BackgroundShape, diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs index 732351682..c1d1ac5e4 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -1,7 +1,8 @@ use super::*; /// A node that can fix its child's width and height. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct FixedNode { /// The fixed width, if any. pub width: Option, diff --git a/src/layout/grid.rs b/src/layout/grid.rs index 33fce0642..b0bf225f7 100644 --- a/src/layout/grid.rs +++ b/src/layout/grid.rs @@ -1,7 +1,8 @@ use super::*; /// A node that arranges its children in a grid. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct GridNode { /// The `main` and `cross` directions of this grid. /// diff --git a/src/layout/image.rs b/src/layout/image.rs index 9ba8cd822..20a521ffc 100644 --- a/src/layout/image.rs +++ b/src/layout/image.rs @@ -4,7 +4,8 @@ use crate::image::ImageId; use ::image::GenericImageView; /// An image node. -#[derive(Debug, Copy, Clone, PartialEq, Hash)] +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct ImageNode { /// The id of the image file. pub id: ImageId, diff --git a/src/layout/incremental.rs b/src/layout/incremental.rs index 9eda04026..1dd90f8ed 100644 --- a/src/layout/incremental.rs +++ b/src/layout/incremental.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "layout-cache")] use std::collections::{hash_map::Entry, HashMap}; use std::ops::Deref; @@ -5,6 +6,7 @@ use super::*; /// Caches layouting artifacts. #[derive(Default, Debug, Clone)] +#[cfg(feature = "layout-cache")] pub struct LayoutCache { /// Maps from node hashes to the resulting frames and regions in which the /// frames are valid. The right hand side of the hash map is a vector of @@ -15,6 +17,7 @@ pub struct LayoutCache { age: usize, } +#[cfg(feature = "layout-cache")] impl LayoutCache { /// Create a new, empty layout cache. pub fn new() -> Self { @@ -100,6 +103,7 @@ impl LayoutCache { /// Cached frames from past layouting. #[derive(Debug, Clone)] +#[cfg(feature = "layout-cache")] pub struct FramesEntry { /// The cached frames for a node. pub frames: Vec>>, @@ -112,6 +116,7 @@ pub struct FramesEntry { temperature: [usize; 5], } +#[cfg(feature = "layout-cache")] impl FramesEntry { /// Construct a new instance. pub fn new(frames: Vec>>, level: usize) -> Self { @@ -205,6 +210,7 @@ impl Constraints { } } + #[cfg(feature = "layout-cache")] fn check(&self, regions: &Regions) -> bool { if self.expand != regions.expand { return false; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index dc819a164..7ba556a11 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -24,9 +24,12 @@ pub use stack::*; use std::any::Any; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; +#[cfg(feature = "layout-cache")] +use std::hash::Hasher; use std::rc::Rc; +#[cfg(feature = "layout-cache")] use fxhash::FxHasher64; use crate::cache::Cache; @@ -35,7 +38,12 @@ use crate::loading::Loader; /// Layout a tree into a collection of frames. pub fn layout(loader: &mut dyn Loader, cache: &mut Cache, tree: &Tree) -> Vec> { - tree.layout(&mut LayoutContext { loader, cache, level: 0 }) + tree.layout(&mut LayoutContext { + loader, + cache, + #[cfg(feature = "layout-cache")] + level: 0, + }) } /// A tree of layout nodes. @@ -77,22 +85,35 @@ impl PageRun { /// A wrapper around a dynamic layouting node. pub struct AnyNode { node: Box, + #[cfg(feature = "layout-cache")] hash: u64, } impl AnyNode { /// Create a new instance from any node that satisifies the required bounds. + #[cfg(feature = "layout-cache")] pub fn new(node: T) -> Self where T: Layout + Debug + Clone + PartialEq + Hash + 'static, { - let mut state = FxHasher64::default(); - node.type_id().hash(&mut state); - node.hash(&mut state); - let hash = state.finish(); + let hash = { + let mut state = FxHasher64::default(); + node.type_id().hash(&mut state); + node.hash(&mut state); + state.finish() + }; Self { node: Box::new(node), hash } } + + /// Create a new instance from any node that satisifies the required bounds. + #[cfg(not(feature = "layout-cache"))] + pub fn new(node: T) -> Self + where + T: Layout + Debug + Clone + PartialEq + 'static, + { + Self { node: Box::new(node) } + } } impl Layout for AnyNode { @@ -101,15 +122,20 @@ impl Layout for AnyNode { ctx: &mut LayoutContext, regions: &Regions, ) -> Vec>> { - ctx.level += 1; - let frames = - ctx.cache.layout.get(self.hash, regions.clone()).unwrap_or_else(|| { - let frames = self.node.layout(ctx, regions); - ctx.cache.layout.insert(self.hash, frames.clone(), ctx.level - 1); - frames - }); - ctx.level -= 1; - frames + #[cfg(feature = "layout-cache")] + { + ctx.level += 1; + let frames = + ctx.cache.layout.get(self.hash, regions.clone()).unwrap_or_else(|| { + let frames = self.node.layout(ctx, regions); + ctx.cache.layout.insert(self.hash, frames.clone(), ctx.level - 1); + frames + }); + ctx.level -= 1; + frames + } + #[cfg(not(feature = "layout-cache"))] + self.node.layout(ctx, regions) } } @@ -117,6 +143,7 @@ impl Clone for AnyNode { fn clone(&self) -> Self { Self { node: self.node.dyn_clone(), + #[cfg(feature = "layout-cache")] hash: self.hash, } } @@ -128,6 +155,7 @@ impl PartialEq for AnyNode { } } +#[cfg(feature = "layout-cache")] impl Hash for AnyNode { fn hash(&self, state: &mut H) { state.write_u64(self.hash); @@ -184,6 +212,7 @@ pub struct LayoutContext<'a> { /// A cache for loaded fonts and artifacts from past layouting. pub cache: &'a mut Cache, /// How deeply nested the current layout tree position is. + #[cfg(feature = "layout-cache")] pub level: usize, } diff --git a/src/layout/pad.rs b/src/layout/pad.rs index 9461f3ff2..75ed366ce 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -1,7 +1,8 @@ use super::*; /// A node that adds padding to its child. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct PadNode { /// The amount of padding. pub padding: Sides, diff --git a/src/layout/par.rs b/src/layout/par.rs index 45eefe29f..464853e02 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -11,7 +11,8 @@ use crate::util::{RangeExt, SliceExt}; type Range = std::ops::Range; /// A node that arranges its children into a paragraph. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct ParNode { /// The inline direction of this paragraph. pub dir: Dir, @@ -22,7 +23,8 @@ pub struct ParNode { } /// A child of a paragraph node. -#[derive(Clone, PartialEq, Hash)] +#[derive(Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub enum ParChild { /// Spacing between other nodes. Spacing(Length), diff --git a/src/layout/stack.rs b/src/layout/stack.rs index fa5fd5840..d8e30b2a3 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -3,7 +3,8 @@ use decorum::N64; use super::*; /// A node that stacks its children. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub struct StackNode { /// The `main` and `cross` directions of this stack. /// @@ -19,7 +20,8 @@ pub struct StackNode { } /// A child of a stack node. -#[derive(Debug, Clone, PartialEq, Hash)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "layout-cache", derive(Hash))] pub enum StackChild { /// Spacing between other nodes. Spacing(Length), diff --git a/tests/typeset.rs b/tests/typeset.rs index 1540164f7..f95c066d8 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -272,46 +272,49 @@ fn test_part( } } - let reference_cache = cache.layout.clone(); - for level in 0 .. reference_cache.levels() { - cache.layout = reference_cache.clone(); - cache.layout.retain(|x| x == level); - if cache.layout.frames.is_empty() { - continue; + #[cfg(feature = "layout-cache")] + { + let reference_cache = cache.layout.clone(); + for level in 0 .. reference_cache.levels() { + cache.layout = reference_cache.clone(); + cache.layout.retain(|x| x == level); + if cache.layout.frames.is_empty() { + continue; + } + + cache.layout.turnaround(); + + let cached_result = layout(loader, cache, &executed.output); + + let misses = cache + .layout + .frames + .iter() + .flat_map(|(_, e)| e) + .filter(|e| e.level == level && !e.hit() && e.age() == 2) + .count(); + + if misses > 0 { + ok = false; + println!( + " Recompilation had {} cache misses on level {} (Subtest {}) ❌", + misses, level, i + ); + } + + if cached_result != layouted { + ok = false; + println!( + " Recompilation of subtest {} differs from clean pass ❌", + i + ); + } } + cache.layout = reference_cache; cache.layout.turnaround(); - - let cached_result = layout(loader, cache, &executed.output); - - let misses = cache - .layout - .frames - .iter() - .flat_map(|(_, e)| e) - .filter(|e| e.level == level && !e.hit() && e.age() == 2) - .count(); - - if misses > 0 { - ok = false; - println!( - " Recompilation had {} cache misses on level {} (Subtest {}) ❌", - misses, level, i - ); - } - - if cached_result != layouted { - ok = false; - println!( - " Recompilation of subtest {} differs from clean pass ❌", - i - ); - } } - cache.layout = reference_cache; - cache.layout.turnaround(); - if !compare_ref { layouted.clear(); }