diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index b223e0bed..6d49da702 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -700,6 +700,11 @@ enum Pass { /// Walk state while paginating. struct Ctx<'a> { doc: &'a Document, + /// The body's sections ([`Document::sections`]) for one pass, with the block index each + /// starts after: header and footer wrap work asks for them per page, and rebuilding the + /// list walked the whole body every time, so a long document paid a body-length scan per + /// page. The document does not change during a pass. + sections: Vec<(usize, &'a SectionProps)>, cache: &'a mut LayoutCache, opts: &'a LayoutOptions, counters: Counters, @@ -2301,6 +2306,7 @@ fn layout_pass( }); let mut ctx = Ctx { doc, + sections: doc.sections(), cache, opts, hide_deleted: opts.hides_deleted(StoryRef::Body), @@ -3391,12 +3397,11 @@ fn footer_table_wraps(ctx: &mut Ctx, pb: &PageBuilder) -> Vec { } ctx.footer_wraps.insert(key, Vec::new()); let doc = ctx.doc; - let sections = doc.sections(); - let Some((_, sect)) = sections.get(page.section) else { return Vec::new() }; + let Some((_, sect)) = ctx.sections.get(page.section).copied() else { return Vec::new() }; if sect.text_direction.is_turned() { return Vec::new(); } - let set = linked(§ions, page.section, |s| s.footers); + let set = linked(&ctx.sections, page.section, |s| s.footers); let id = if sect.title_page && first { set.first } else if doc.settings.even_odd_headers && page.number.is_multiple_of(2) { @@ -3408,7 +3413,7 @@ fn footer_table_wraps(ctx: &mut Ctx, pb: &PageBuilder) -> Vec { let Some(part) = doc.parts.get(&id) else { return Vec::new() }; let blocks = part.blocks.clone(); let x = sect.margin_left + sect.gutter; - let base = defining_width(§ions, page.section, |s| s.footers, Some(id)); + let base = defining_width(&ctx.sections, page.section, |s| s.footers, Some(id)); let counters = std::mem::take(&mut ctx.counters); let boxes = std::mem::take(&mut ctx.boxes); let chains = std::mem::take(&mut ctx.chains); @@ -4526,12 +4531,11 @@ fn header_table_areas(ctx: &mut Ctx, pb: &PageBuilder) -> Vec { if let Some(areas) = ctx.header_tables.get(&key) { return areas.clone(); } - let sections = ctx.doc.sections(); - let Some((_, sect)) = sections.get(page.section) else { return Vec::new() }; + let Some((_, sect)) = ctx.sections.get(page.section).copied() else { return Vec::new() }; if sect.text_direction.is_turned() { return Vec::new(); } - let set = linked(§ions, page.section, |s| s.headers); + let set = linked(&ctx.sections, page.section, |s| s.headers); let id = if sect.title_page && first { set.first } else if ctx.doc.settings.even_odd_headers && page.number.is_multiple_of(2) { @@ -4544,7 +4548,7 @@ fn header_table_areas(ctx: &mut Ctx, pb: &PageBuilder) -> Vec { let blocks = part.blocks.clone(); let x = sect.margin_left + sect.gutter; let frame = PageFrame { sect, origin: (x, sect.header), number: page.number, mirror: ctx.mirror }; - let base = defining_width(§ions, page.section, |s| s.headers, Some(id)); + let base = defining_width(&ctx.sections, page.section, |s| s.headers, Some(id)); let counters = std::mem::take(&mut ctx.counters); let boxes = std::mem::take(&mut ctx.boxes); let chains = std::mem::take(&mut ctx.chains); diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 5ae9b53a1..30240a273 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -849,6 +849,26 @@ fn hidden_separators_with_automatic_hyphenation_lay_out_in_linear_time() { assert!(ratio < 24.0, "8x the text took {ratio:.1}x the time ({t1:.4} s → {tk:.4} s): not linear"); } +/// n one-line paragraphs, each starting a page. +fn page_break_paragraphs(n: usize) -> Document { + let mut d = Document::from_text(&"hello\n".repeat(n)); + for i in 0..n { + d.para_mut(StoryRef::Body, &Path::top(i)).unwrap().props.page_break_before = Some(true); + } + d +} + +/// A document of many paragraphs lays out in linear time. Every page asks the layout for the +/// section properties (for the wrap areas of the headers and footers), and that used to rebuild +/// the section list by walking the whole body, so a document of n paragraphs paid n body-length +/// scans: a two-megabyte file took half a minute before the first page was drawn. +#[test] +fn a_document_of_many_paragraphs_lays_out_in_linear_time() { + let (t1, tk, ratio) = layout_time_ratio(page_break_paragraphs, 2_500, &LayoutOptions::default()); + eprintln!("t(n) {t1:.4} s, t(8n) {tk:.4} s, ratio {ratio:.1}"); + assert!(ratio < 24.0, "8x the paragraphs took {ratio:.1}x the time ({t1:.4} s → {tk:.4} s): not linear"); +} + #[test] fn many_hidden_float_anchors_lay_out_in_linear_time() { // n square-wrapped shapes, each anchored in its own hidden run (bold alternating, so runs do not