diff --git a/Cargo.lock b/Cargo.lock index f20c5270..64610cc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2722,7 +2722,9 @@ dependencies = [ "notify", "once_cell", "open", + "parking_lot", "pathdiff", + "rayon", "rustls", "rustls-pemfile", "same-file", diff --git a/Cargo.toml b/Cargo.toml index 2163d7c0..9e6e5ac0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ once_cell = "1" open = "5.0.1" oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } +parking_lot = "0.12.1" pathdiff = "0.2" pdf-writer = "0.9.2" pixglyph = "0.2" diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index c148bdb3..702746f5 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -37,7 +37,9 @@ inferno = { workspace = true } notify = { workspace = true } once_cell = { workspace = true } open = { workspace = true } +parking_lot = { workspace = true } pathdiff = { workspace = true } +rayon = { workspace = true } rustls = { workspace = true } rustls-pemfile = { workspace = true } same-file = { workspace = true } diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index dd15c8fc..5916f18b 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -4,7 +4,9 @@ use std::path::{Path, PathBuf}; use chrono::{Datelike, Timelike}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::{self, termcolor}; -use ecow::eco_format; +use ecow::{eco_format, EcoString}; +use parking_lot::RwLock; +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use termcolor::{ColorChoice, StandardStream}; use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult}; use typst::eval::Tracer; @@ -214,39 +216,48 @@ fn export_image( // first page should be numbered "001" if there are between 100 and // 999 pages. let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize; - let mut storage; let cache = world.export_cache(); - for (i, frame) in document.pages.iter().enumerate() { - let path = if numbered { - storage = string.replace("{n}", &format!("{:0width$}", i + 1)); - Path::new(&storage) - } else { - output.as_path() - }; - // If we are not watching, don't use the cache. - // If the frame is in the cache, skip it. - // If the file does not exist, always create it. - if watching && cache.is_cached(i, frame) && path.exists() { - continue; - } + // The results are collected in a `Vec<()>` which does not allocate. + document + .pages + .par_iter() + .enumerate() + .map(|(i, frame)| { + let storage; + let path = if numbered { + storage = string.replace("{n}", &format!("{:0width$}", i + 1)); + Path::new(&storage) + } else { + output.as_path() + }; - match fmt { - ImageExportFormat::Png => { - let pixmap = - typst_render::render(frame, command.ppi / 72.0, Color::WHITE); - pixmap - .save_png(path) - .map_err(|err| eco_format!("failed to write PNG file ({err})"))?; + // If we are not watching, don't use the cache. + // If the frame is in the cache, skip it. + // If the file does not exist, always create it. + if watching && cache.is_cached(i, frame) && path.exists() { + return Ok(()); } - ImageExportFormat::Svg => { - let svg = typst_svg::svg(frame); - fs::write(path, svg.as_bytes()) - .map_err(|err| eco_format!("failed to write SVG file ({err})"))?; + + match fmt { + ImageExportFormat::Png => { + let pixmap = + typst_render::render(frame, command.ppi / 72.0, Color::WHITE); + pixmap + .save_png(path) + .map_err(|err| eco_format!("failed to write PNG file ({err})"))?; + } + ImageExportFormat::Svg => { + let svg = typst_svg::svg(frame); + fs::write(path, svg.as_bytes()) + .map_err(|err| eco_format!("failed to write SVG file ({err})"))?; + } } - } - } + + Ok(()) + }) + .collect::, EcoString>>()?; Ok(()) } @@ -260,26 +271,27 @@ fn export_image( /// complexity and memory usage of such a cache. pub struct ExportCache { /// The hashes of last compilation's frames. - pub cache: Vec, + pub cache: RwLock>, } impl ExportCache { /// Creates a new export cache. pub fn new() -> Self { - Self { cache: Vec::with_capacity(32) } + Self { cache: RwLock::new(Vec::with_capacity(32)) } } /// Returns true if the entry is cached and appends the new hash to the /// cache (for the next compilation). - pub fn is_cached(&mut self, i: usize, frame: &Frame) -> bool { + pub fn is_cached(&self, i: usize, frame: &Frame) -> bool { let hash = typst::util::hash128(frame); - if i >= self.cache.len() { - self.cache.push(hash); + let mut cache = self.cache.upgradable_read(); + if i >= cache.len() { + cache.with_upgraded(|cache| cache.push(hash)); return false; } - std::mem::replace(&mut self.cache[i], hash) == hash + cache.with_upgraded(|cache| std::mem::replace(&mut cache[i], hash) == hash) } } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index cd4244fc..095458df 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::{OnceLock, RwLock}; +use std::sync::OnceLock; use std::{fs, mem}; use chrono::{DateTime, Datelike, Local}; use comemo::Prehashed; use ecow::eco_format; +use parking_lot::Mutex; use typst::diag::{FileError, FileResult, StrResult}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::syntax::{FileId, Source, VirtualPath}; @@ -34,7 +35,7 @@ pub struct SystemWorld { /// Locations of and storage for lazily loaded fonts. fonts: Vec, /// Maps file ids to source files and buffers. - slots: RwLock>, + slots: Mutex>, /// The current datetime if requested. This is stored here to ensure it is /// always the same within one compilation. Reset between compilations. now: OnceLock>, @@ -89,7 +90,7 @@ impl SystemWorld { library: Prehashed::new(library), book: Prehashed::new(searcher.book), fonts: searcher.fonts, - slots: RwLock::new(HashMap::new()), + slots: Mutex::new(HashMap::new()), now: OnceLock::new(), export_cache: ExportCache::new(), }) @@ -114,7 +115,6 @@ impl SystemWorld { pub fn dependencies(&mut self) -> impl Iterator + '_ { self.slots .get_mut() - .unwrap() .values() .filter(|slot| slot.accessed()) .filter_map(|slot| system_path(&self.root, slot.id).ok()) @@ -122,7 +122,7 @@ impl SystemWorld { /// Reset the compilation state in preparation of a new compilation. pub fn reset(&mut self) { - for slot in self.slots.get_mut().unwrap().values_mut() { + for slot in self.slots.get_mut().values_mut() { slot.reset(); } self.now.take(); @@ -140,8 +140,8 @@ impl SystemWorld { } /// Gets access to the export cache. - pub fn export_cache(&mut self) -> &mut ExportCache { - &mut self.export_cache + pub fn export_cache(&self) -> &ExportCache { + &self.export_cache } } @@ -192,7 +192,7 @@ impl SystemWorld { where F: FnOnce(&mut FileSlot) -> T, { - let mut map = self.slots.write().unwrap(); + let mut map = self.slots.lock(); f(map.entry(id).or_insert_with(|| FileSlot::new(id))) } } diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 3982d156..dba4e3dc 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -290,6 +290,13 @@ fn deflate_memoized(content: &[u8]) -> Arc> { Arc::new(deflate(content)) } +/// Memoized and deferred version of [`deflate`] specialized for a page's content +/// stream. +#[comemo::memoize] +fn deflate_deferred(content: Vec) -> Deferred> { + Deferred::new(move || deflate(&content)) +} + /// Create a base64-encoded hash of the value. fn hash_base64(value: &T) -> String { base64::engine::general_purpose::STANDARD diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index e4322f5f..05501d2c 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -15,7 +15,7 @@ use typst::layout::{ }; use typst::model::Destination; use typst::text::{Font, TextItem}; -use typst::util::Numeric; +use typst::util::{Deferred, Numeric}; use typst::visualize::{ FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, }; @@ -23,7 +23,7 @@ use typst::visualize::{ use crate::color::PaintEncode; use crate::extg::ExtGState; use crate::image::deferred_image; -use crate::{deflate_memoized, AbsExt, EmExt, PdfContext}; +use crate::{deflate_deferred, AbsExt, EmExt, PdfContext}; /// Construct page objects. #[tracing::instrument(skip_all)] @@ -71,7 +71,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page) let page = Page { size, - content: ctx.content.finish(), + content: deflate_deferred(ctx.content.finish()), id: ctx.page_ref, uses_opacities: ctx.uses_opacities, links: ctx.links, @@ -198,8 +198,9 @@ fn write_page(ctx: &mut PdfContext, i: usize) { annotations.finish(); page_writer.finish(); - let data = deflate_memoized(&page.content); - ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode); + ctx.pdf + .stream(content_id, page.content.wait()) + .filter(Filter::FlateDecode); } /// Write the page labels. @@ -258,7 +259,7 @@ pub struct Page { /// The page's dimensions. pub size: Size, /// The page's content stream. - pub content: Vec, + pub content: Deferred>, /// Whether the page uses opacities. pub uses_opacities: bool, /// Links in the PDF coordinate system. diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs index 296760dd..ea7d48e0 100644 --- a/crates/typst-pdf/src/pattern.rs +++ b/crates/typst-pdf/src/pattern.rs @@ -7,7 +7,7 @@ use typst::visualize::{Pattern, RelativeTo}; use crate::color::PaintEncode; use crate::page::{construct_page, PageContext, PageResource, ResourceKind, Transforms}; -use crate::{deflate_memoized, transform_to_array, PdfContext}; +use crate::{transform_to_array, PdfContext}; /// Writes the actual patterns (tiling patterns) to the PDF. /// This is performed once after writing all pages. @@ -16,8 +16,7 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) { let tiling = ctx.alloc.bump(); ctx.pattern_refs.push(tiling); - let content = deflate_memoized(content); - let mut tiling_pattern = ctx.pdf.tiling_pattern(tiling, &content); + let mut tiling_pattern = ctx.pdf.tiling_pattern(tiling, content); tiling_pattern .tiling_type(TilingType::ConstantSpacing) .paint_type(PaintType::Colored) @@ -81,7 +80,7 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) { } /// A pattern and its transform. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Clone, PartialEq, Eq, Hash)] pub struct PdfPattern { /// The transform to apply to the gradient. pub transform: Transform, @@ -120,7 +119,7 @@ fn register_pattern( let pdf_pattern = PdfPattern { transform, pattern: pattern.clone(), - content: content.content, + content: content.content.wait().clone(), resources: content.resources.into_iter().collect(), };