Merge PR #4090: Layout: a document of many pages paid a body-length scan per page

This commit is contained in:
aiongg
2026-10-24 20:38:44 +00:00
2 changed files with 32 additions and 8 deletions
+12 -8
View File
@@ -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<WrapArea> {
}
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(&sections, 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<WrapArea> {
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(&sections, 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<Rect> {
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(&sections, 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<Rect> {
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(&sections, 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);
+20
View File
@@ -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