Parallel export (#2989)

This commit is contained in:
Sébastien d'Herbais de Thun 2023-12-18 12:32:53 +01:00 committed by GitHub
parent f3c39ac84a
commit 08225e42d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 77 additions and 53 deletions

2
Cargo.lock generated
View File

@ -2722,7 +2722,9 @@ dependencies = [
"notify", "notify",
"once_cell", "once_cell",
"open", "open",
"parking_lot",
"pathdiff", "pathdiff",
"rayon",
"rustls", "rustls",
"rustls-pemfile", "rustls-pemfile",
"same-file", "same-file",

View File

@ -67,6 +67,7 @@ once_cell = "1"
open = "5.0.1" open = "5.0.1"
oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] } oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
parking_lot = "0.12.1"
pathdiff = "0.2" pathdiff = "0.2"
pdf-writer = "0.9.2" pdf-writer = "0.9.2"
pixglyph = "0.2" pixglyph = "0.2"

View File

@ -37,7 +37,9 @@ inferno = { workspace = true }
notify = { workspace = true } notify = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
open = { workspace = true } open = { workspace = true }
parking_lot = { workspace = true }
pathdiff = { workspace = true } pathdiff = { workspace = true }
rayon = { workspace = true }
rustls = { workspace = true } rustls = { workspace = true }
rustls-pemfile = { workspace = true } rustls-pemfile = { workspace = true }
same-file = { workspace = true } same-file = { workspace = true }

View File

@ -4,7 +4,9 @@ use std::path::{Path, PathBuf};
use chrono::{Datelike, Timelike}; use chrono::{Datelike, Timelike};
use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term::{self, termcolor}; 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 termcolor::{ColorChoice, StandardStream};
use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult}; use typst::diag::{bail, At, Severity, SourceDiagnostic, StrResult};
use typst::eval::Tracer; use typst::eval::Tracer;
@ -214,10 +216,16 @@ fn export_image(
// first page should be numbered "001" if there are between 100 and // first page should be numbered "001" if there are between 100 and
// 999 pages. // 999 pages.
let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize; let width = 1 + document.pages.len().checked_ilog10().unwrap_or(0) as usize;
let mut storage;
let cache = world.export_cache(); let cache = world.export_cache();
for (i, frame) in document.pages.iter().enumerate() {
// 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 { let path = if numbered {
storage = string.replace("{n}", &format!("{:0width$}", i + 1)); storage = string.replace("{n}", &format!("{:0width$}", i + 1));
Path::new(&storage) Path::new(&storage)
@ -229,7 +237,7 @@ fn export_image(
// If the frame is in the cache, skip it. // If the frame is in the cache, skip it.
// If the file does not exist, always create it. // If the file does not exist, always create it.
if watching && cache.is_cached(i, frame) && path.exists() { if watching && cache.is_cached(i, frame) && path.exists() {
continue; return Ok(());
} }
match fmt { match fmt {
@ -246,7 +254,10 @@ fn export_image(
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?; .map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
} }
} }
}
Ok(())
})
.collect::<Result<Vec<()>, EcoString>>()?;
Ok(()) Ok(())
} }
@ -260,26 +271,27 @@ fn export_image(
/// complexity and memory usage of such a cache. /// complexity and memory usage of such a cache.
pub struct ExportCache { pub struct ExportCache {
/// The hashes of last compilation's frames. /// The hashes of last compilation's frames.
pub cache: Vec<u128>, pub cache: RwLock<Vec<u128>>,
} }
impl ExportCache { impl ExportCache {
/// Creates a new export cache. /// Creates a new export cache.
pub fn new() -> Self { 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 /// Returns true if the entry is cached and appends the new hash to the
/// cache (for the next compilation). /// 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); let hash = typst::util::hash128(frame);
if i >= self.cache.len() { let mut cache = self.cache.upgradable_read();
self.cache.push(hash); if i >= cache.len() {
cache.with_upgraded(|cache| cache.push(hash));
return false; return false;
} }
std::mem::replace(&mut self.cache[i], hash) == hash cache.with_upgraded(|cache| std::mem::replace(&mut cache[i], hash) == hash)
} }
} }

View File

@ -1,11 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{OnceLock, RwLock}; use std::sync::OnceLock;
use std::{fs, mem}; use std::{fs, mem};
use chrono::{DateTime, Datelike, Local}; use chrono::{DateTime, Datelike, Local};
use comemo::Prehashed; use comemo::Prehashed;
use ecow::eco_format; use ecow::eco_format;
use parking_lot::Mutex;
use typst::diag::{FileError, FileResult, StrResult}; use typst::diag::{FileError, FileResult, StrResult};
use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
@ -34,7 +35,7 @@ pub struct SystemWorld {
/// Locations of and storage for lazily loaded fonts. /// Locations of and storage for lazily loaded fonts.
fonts: Vec<FontSlot>, fonts: Vec<FontSlot>,
/// Maps file ids to source files and buffers. /// Maps file ids to source files and buffers.
slots: RwLock<HashMap<FileId, FileSlot>>, slots: Mutex<HashMap<FileId, FileSlot>>,
/// The current datetime if requested. This is stored here to ensure it is /// The current datetime if requested. This is stored here to ensure it is
/// always the same within one compilation. Reset between compilations. /// always the same within one compilation. Reset between compilations.
now: OnceLock<DateTime<Local>>, now: OnceLock<DateTime<Local>>,
@ -89,7 +90,7 @@ impl SystemWorld {
library: Prehashed::new(library), library: Prehashed::new(library),
book: Prehashed::new(searcher.book), book: Prehashed::new(searcher.book),
fonts: searcher.fonts, fonts: searcher.fonts,
slots: RwLock::new(HashMap::new()), slots: Mutex::new(HashMap::new()),
now: OnceLock::new(), now: OnceLock::new(),
export_cache: ExportCache::new(), export_cache: ExportCache::new(),
}) })
@ -114,7 +115,6 @@ impl SystemWorld {
pub fn dependencies(&mut self) -> impl Iterator<Item = PathBuf> + '_ { pub fn dependencies(&mut self) -> impl Iterator<Item = PathBuf> + '_ {
self.slots self.slots
.get_mut() .get_mut()
.unwrap()
.values() .values()
.filter(|slot| slot.accessed()) .filter(|slot| slot.accessed())
.filter_map(|slot| system_path(&self.root, slot.id).ok()) .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. /// Reset the compilation state in preparation of a new compilation.
pub fn reset(&mut self) { 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(); slot.reset();
} }
self.now.take(); self.now.take();
@ -140,8 +140,8 @@ impl SystemWorld {
} }
/// Gets access to the export cache. /// Gets access to the export cache.
pub fn export_cache(&mut self) -> &mut ExportCache { pub fn export_cache(&self) -> &ExportCache {
&mut self.export_cache &self.export_cache
} }
} }
@ -192,7 +192,7 @@ impl SystemWorld {
where where
F: FnOnce(&mut FileSlot) -> T, 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))) f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
} }
} }

View File

@ -290,6 +290,13 @@ fn deflate_memoized(content: &[u8]) -> Arc<Vec<u8>> {
Arc::new(deflate(content)) Arc::new(deflate(content))
} }
/// Memoized and deferred version of [`deflate`] specialized for a page's content
/// stream.
#[comemo::memoize]
fn deflate_deferred(content: Vec<u8>) -> Deferred<Vec<u8>> {
Deferred::new(move || deflate(&content))
}
/// Create a base64-encoded hash of the value. /// Create a base64-encoded hash of the value.
fn hash_base64<T: Hash>(value: &T) -> String { fn hash_base64<T: Hash>(value: &T) -> String {
base64::engine::general_purpose::STANDARD base64::engine::general_purpose::STANDARD

View File

@ -15,7 +15,7 @@ use typst::layout::{
}; };
use typst::model::Destination; use typst::model::Destination;
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::util::Numeric; use typst::util::{Deferred, Numeric};
use typst::visualize::{ use typst::visualize::{
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
}; };
@ -23,7 +23,7 @@ use typst::visualize::{
use crate::color::PaintEncode; use crate::color::PaintEncode;
use crate::extg::ExtGState; use crate::extg::ExtGState;
use crate::image::deferred_image; use crate::image::deferred_image;
use crate::{deflate_memoized, AbsExt, EmExt, PdfContext}; use crate::{deflate_deferred, AbsExt, EmExt, PdfContext};
/// Construct page objects. /// Construct page objects.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -71,7 +71,7 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page)
let page = Page { let page = Page {
size, size,
content: ctx.content.finish(), content: deflate_deferred(ctx.content.finish()),
id: ctx.page_ref, id: ctx.page_ref,
uses_opacities: ctx.uses_opacities, uses_opacities: ctx.uses_opacities,
links: ctx.links, links: ctx.links,
@ -198,8 +198,9 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
annotations.finish(); annotations.finish();
page_writer.finish(); page_writer.finish();
let data = deflate_memoized(&page.content); ctx.pdf
ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode); .stream(content_id, page.content.wait())
.filter(Filter::FlateDecode);
} }
/// Write the page labels. /// Write the page labels.
@ -258,7 +259,7 @@ pub struct Page {
/// The page's dimensions. /// The page's dimensions.
pub size: Size, pub size: Size,
/// The page's content stream. /// The page's content stream.
pub content: Vec<u8>, pub content: Deferred<Vec<u8>>,
/// Whether the page uses opacities. /// Whether the page uses opacities.
pub uses_opacities: bool, pub uses_opacities: bool,
/// Links in the PDF coordinate system. /// Links in the PDF coordinate system.

View File

@ -7,7 +7,7 @@ use typst::visualize::{Pattern, RelativeTo};
use crate::color::PaintEncode; use crate::color::PaintEncode;
use crate::page::{construct_page, PageContext, PageResource, ResourceKind, Transforms}; 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. /// Writes the actual patterns (tiling patterns) to the PDF.
/// This is performed once after writing all pages. /// 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(); let tiling = ctx.alloc.bump();
ctx.pattern_refs.push(tiling); 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_pattern
.tiling_type(TilingType::ConstantSpacing) .tiling_type(TilingType::ConstantSpacing)
.paint_type(PaintType::Colored) .paint_type(PaintType::Colored)
@ -81,7 +80,7 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) {
} }
/// A pattern and its transform. /// A pattern and its transform.
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Clone, PartialEq, Eq, Hash)]
pub struct PdfPattern { pub struct PdfPattern {
/// The transform to apply to the gradient. /// The transform to apply to the gradient.
pub transform: Transform, pub transform: Transform,
@ -120,7 +119,7 @@ fn register_pattern(
let pdf_pattern = PdfPattern { let pdf_pattern = PdfPattern {
transform, transform,
pattern: pattern.clone(), pattern: pattern.clone(),
content: content.content, content: content.content.wait().clone(),
resources: content.resources.into_iter().collect(), resources: content.resources.into_iter().collect(),
}; };