summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/rust.yml9
-rw-r--r--Cargo.toml3
-rw-r--r--bench/Cargo.toml8
-rw-r--r--bench/src/bench.rs163
-rw-r--r--src/cache.rs3
-rw-r--r--src/layout/background.rs3
-rw-r--r--src/layout/fixed.rs3
-rw-r--r--src/layout/grid.rs3
-rw-r--r--src/layout/image.rs3
-rw-r--r--src/layout/incremental.rs6
-rw-r--r--src/layout/mod.rs59
-rw-r--r--src/layout/pad.rs3
-rw-r--r--src/layout/par.rs6
-rw-r--r--src/layout/stack.rs6
-rw-r--r--tests/typeset.rs67
15 files changed, 257 insertions, 88 deletions
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 43fec7d6..7c00dab4 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 b18b0af5..2cb23c4e 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 4965c3ea..260a2ba1 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 832d538f..c86ccc89 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!
- 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));
+ bench!("parse", case.parse());
+ bench!("eval", case.eval());
+ bench!("exec", case.exec());
+
+ #[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<RefCell<Self>> {
+ 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<RefCell<Context>>,
+ src: String,
+ scope: Scope,
+ state: State,
+ ast: Rc<syntax::Tree>,
+ module: Module,
+ tree: layout::Tree,
+ frames: Vec<Rc<Frame>>,
+}
+
+impl Case {
+ fn new(src: impl Into<String>, ctx: Rc<RefCell<Context>>) -> 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<Rc<Frame>> {
+ let mut borrowed = self.ctx.borrow_mut();
+ let Context { loader, cache } = &mut *borrowed;
+ layout(loader, cache, &self.tree)
+ }
+
+ fn typeset(&self) -> Vec<Rc<Frame>> {
+ 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<u8> {
+ let ctx = self.ctx.borrow();
+ pdf(&ctx.cache, &self.frames)
}
}
diff --git a/src/cache.rs b/src/cache.rs
index aa9c10a0..2aa276aa 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 8390a756..013a887a 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 73235168..c1d1ac5e 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<Linear>,
diff --git a/src/layout/grid.rs b/src/layout/grid.rs
index 33fce064..b0bf225f 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 9ba8cd82..20a521ff 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 9eda0402..1dd90f8e 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<Constrained<Rc<Frame>>>,
@@ -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<Constrained<Rc<Frame>>>, 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 dc819a16..7ba556a1 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<Rc<Frame>> {
- 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<dyn Bounds>,
+ #[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<T>(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<T>(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<Constrained<Rc<Frame>>> {
- 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<H: Hasher>(&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 9461f3ff..75ed366c 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<Linear>,
diff --git a/src/layout/par.rs b/src/layout/par.rs
index 45eefe29..464853e0 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<usize>;
/// 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 fa5fd584..d8e30b2a 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 1540164f..f95c066d 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -272,45 +272,48 @@ 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();
+ cache.layout.turnaround();
- let cached_result = layout(loader, cache, &executed.output);
+ 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();
+ 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 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
- );
+ if cached_result != layouted {
+ ok = false;
+ println!(
+ " Recompilation of subtest {} differs from clean pass ❌",
+ i
+ );
+ }
}
- }
- cache.layout = reference_cache;
- cache.layout.turnaround();
+ cache.layout = reference_cache;
+ cache.layout.turnaround();
+ }
if !compare_ref {
layouted.clear();