From cd535323714152b4f45db07e080acfda67861670 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 15 Oct 2026 15:45:27 +0800 Subject: [PATCH] perf(layout): retain supported table construction graphs Validate source-backed table roles against the shared normalized graph and refresh internal table sizing consistently. Under retention pressure, compact the graph before dropping reusable inline measurements. --- .../src/builder/retained_construction.rs | 60 ++- .../src/taffy_tree/measurement_tests.rs | 7 +- moli-layout/src/working_set.rs | 16 +- .../working_set/owned_construction_tests.rs | 6 + .../owned_construction_tests/table.rs | 379 ++++++++++++++++++ moli-layout/src/world.rs | 23 +- .../tests/realtime_working_sets/table.rs | 49 +++ 7 files changed, 507 insertions(+), 33 deletions(-) create mode 100644 moli-layout/src/working_set/owned_construction_tests/table.rs diff --git a/moli-layout/src/builder/retained_construction.rs b/moli-layout/src/builder/retained_construction.rs index ab5c3351b2..f07336a755 100644 --- a/moli-layout/src/builder/retained_construction.rs +++ b/moli-layout/src/builder/retained_construction.rs @@ -45,26 +45,41 @@ impl ElementShape { return Some(Self::Suppressed); } let kind = principal_kind(semantics, style); - if semantics.category != LayoutElementCategory::Generic - || !matches!( - style.display(), - LayoutDisplay::Block | LayoutDisplay::FlowRoot | LayoutDisplay::Grid - ) - || semantics - .category - .child_box_policy(semantics.is_replaced(), style.display()) - == crate::ChildBoxPolicy::None + if !matches!( + semantics.category, + LayoutElementCategory::Generic | LayoutElementCategory::Table(_) + ) || !matches!( + style.display(), + LayoutDisplay::Block + | LayoutDisplay::FlowRoot + | LayoutDisplay::Grid + | LayoutDisplay::Table + | LayoutDisplay::InlineTable + | LayoutDisplay::TableCaption + | LayoutDisplay::TableRowGroup + | LayoutDisplay::TableHeaderGroup + | LayoutDisplay::TableFooterGroup + | LayoutDisplay::TableRow + | LayoutDisplay::TableCell + | LayoutDisplay::TableColumnGroup + | LayoutDisplay::TableColumn + ) || (semantics + .category + .child_box_policy(semantics.is_replaced(), style.display()) + == crate::ChildBoxPolicy::None + && kind != LayoutBoxKind::TableColumn) || style.is_floated() || before || after { return None; } - // Normal flow and Grid use different whitespace/item normalization. - // Admit only the shared builder's final graph without anonymous boxes; - // direct meaningful Grid text and repaired structures still fall back. - // Eligible block-level Grid items need no display blockification. - // Numeric parents, placement and order are rebuilt from current styles. + // Admit the shared builder's actual source-backed graph, not a second + // set of flow/Grid/table normalization rules. Anonymous repairs and + // discarded principal styles are checked against the finished graph. + // A table column is a source-backed leaf with no private replaced or + // form-control contents; its unconsumed descendants stay outside the + // recorded frontier. Numeric parents and table/grid data stay fresh. Some(Self::Principal { kind, display: style.display(), @@ -215,6 +230,16 @@ where LayoutBoxKind::PrincipalBlock | LayoutBoxKind::PrincipalFlowRoot | LayoutBoxKind::PrincipalGrid + | LayoutBoxKind::TableWrapper + | LayoutBoxKind::InlineTableWrapper + | LayoutBoxKind::TableCaption + | LayoutBoxKind::TableRowGroup + | LayoutBoxKind::TableHeaderGroup + | LayoutBoxKind::TableFooterGroup + | LayoutBoxKind::TableRow + | LayoutBoxKind::TableCell + | LayoutBoxKind::TableColumnGroup + | LayoutBoxKind::TableColumn | LayoutBoxKind::Text ) && layout_box.structural_parent == layout_box.parent @@ -222,6 +247,13 @@ where .parent .is_none_or(|parent| parent.index() < index) }) + // Styles are restored in produced principal-box order. Table + // column groups may discard otherwise eligible element subtrees; + // those frontiers must not leave unmatched style records here. + && self.sources.iter().filter_map(|input| match &input.kind { + SourceInputKind::Element { shape: ElementShape::Principal { .. }, .. } => Some(input.node), + _ => None, + }).eq(world.boxes.iter().filter(|layout_box| layout_box.kind != LayoutBoxKind::Text).map(|layout_box| layout_box.source.expect("supported graph has sources"))) } /// Charge visible allocations by capacity. Shared source text and diff --git a/moli-layout/src/taffy_tree/measurement_tests.rs b/moli-layout/src/taffy_tree/measurement_tests.rs index 68606b226a..fca174d5bd 100644 --- a/moli-layout/src/taffy_tree/measurement_tests.rs +++ b/moli-layout/src/taffy_tree/measurement_tests.rs @@ -309,7 +309,10 @@ fn inline_measurement_fixed_width_survives_unused_parent_width_changes() { ..wide }, LayoutInput { - definite_dimensions: Size::NONE, + known_dimensions_are_definite: Size { + width: false, + height: false, + }, ..wide }, LayoutInput { @@ -344,7 +347,7 @@ fn inline_measurement_width_dependencies_remain_in_the_key() { let mut narrow = inputs(); if dependency == 0 { narrow.known_dimensions.width = None; - narrow.definite_dimensions.width = None; + narrow.known_dimensions_are_definite.width = false; } else { for world in [&mut world, &mut cold] { let style = &mut world.boxes[0].style.taffy; diff --git a/moli-layout/src/working_set.rs b/moli-layout/src/working_set.rs index 8119217fc6..5ce91da9bb 100644 --- a/moli-layout/src/working_set.rs +++ b/moli-layout/src/working_set.rs @@ -488,13 +488,6 @@ where } let mut retained = RetainedWork::from_world(world, fonts, pass_box_count); let mut estimate = retained.estimated_retention(); - if estimate.total() > self.limits.max_estimated_retained_bytes { - // Measurements can be recomputed without discarding a supported - // graph or its accepted final lines. Admit this additional layer - // only when it fits the existing document budget. - retained.discard_inline_measurements(); - estimate = retained.estimated_retention(); - } if retained.owned_construction().is_some() { if estimate.total() > self.limits.max_estimated_retained_bytes { self.stats.owned_box_capacity_trimmed = retained.trim_owned_box_capacity(); @@ -506,6 +499,15 @@ where estimate = retained.estimated_retention(); } } + if estimate.total() > self.limits.max_estimated_retained_bytes { + // Prefer a compact validated structure with useful text results + // over keeping full box storage by prematurely evicting them. + // Both graph representations use the same numeric backend. + // Discard the measurement layer only if the compact candidate + // also exceeds the unchanged document admission limit. + retained.discard_inline_measurements(); + estimate = retained.estimated_retention(); + } let estimated_retained_bytes = estimate.total(); self.stats.candidate_estimated_bytes = estimated_retained_bytes; self.stats.estimated_graph_bytes = estimate.graph; diff --git a/moli-layout/src/working_set/owned_construction_tests.rs b/moli-layout/src/working_set/owned_construction_tests.rs index e141efcde2..03ef6e2253 100644 --- a/moli-layout/src/working_set/owned_construction_tests.rs +++ b/moli-layout/src/working_set/owned_construction_tests.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; mod grid; +mod table; use super::*; use crate::{ @@ -37,6 +38,7 @@ impl Node { #[derive(Clone)] struct Source { nodes: Vec, + semantics: HashMap, hidden_name: Arc, scroll: LayoutPoint, selection: Option, @@ -54,6 +56,7 @@ impl Source { Node::text("second paragraph"), Node::element(Vec::new()), ], + semantics: HashMap::new(), hidden_name: Arc::from("owned-hidden-dependency"), scroll: LayoutPoint::ZERO, selection: None, @@ -90,6 +93,9 @@ impl LayoutSource for Source { fn element_semantics(&self, node: usize) -> Option { (self.node_kind(node) == LayoutSourceKind::Element).then(|| { + if let Some(semantics) = self.semantics.get(&node) { + return semantics.clone(); + } LayoutElementSemantics::new( LayoutNamespace::Html, if node == 5 { diff --git a/moli-layout/src/working_set/owned_construction_tests/table.rs b/moli-layout/src/working_set/owned_construction_tests/table.rs new file mode 100644 index 0000000000..4df24f6389 --- /dev/null +++ b/moli-layout/src/working_set/owned_construction_tests/table.rs @@ -0,0 +1,379 @@ +use super::*; +use crate::LayoutTableRole; + +struct TableStyles { + base: Styles, + displays: HashMap, + aspect_ratio: Option, +} + +impl LayoutStyleResolver for TableStyles { + fn element_styles( + &mut self, + node: usize, + ) -> Result, LayoutError> { + let Some(styles) = self.base.element_styles(node)? else { + return Ok(None); + }; + let (mut style, _, _) = styles.into_parts(); + let display = self + .displays + .get(&node) + .copied() + .unwrap_or(LayoutDisplay::Block); + style.force_layout_display(display); + style.taffy.size = taffy::Size { + width: taffy::Dimension::auto(), + height: taffy::Dimension::auto(), + }; + if node == 0 { + style.taffy.size.width = taffy::Dimension::length(320.0); + } + if node == 4 { + style.taffy.size.width = taffy::Dimension::length(self.base.first_width); + } + style.taffy.aspect_ratio = self.aspect_ratio; + Ok(Some(ResolvedLayoutElementStyles::from_primary(style))) + } +} + +fn fixture() -> (Source, TableStyles) { + let mut source = Source::new(); + source.nodes = vec![ + Node::element(vec![1]), + Node::element(vec![3]), + Node::text("first paragraph"), + Node::element(vec![4, 5]), + Node::element(vec![2]), + Node::element(vec![6]), + Node::text("second paragraph"), + ]; + for (node, tag, role) in [ + (0, "table", LayoutTableRole::Table), + (1, "tbody", LayoutTableRole::BodyGroup), + (3, "tr", LayoutTableRole::Row), + (4, "td", LayoutTableRole::Cell), + (5, "td", LayoutTableRole::Cell), + ] { + source.semantics.insert( + node, + LayoutElementSemantics::new( + LayoutNamespace::Html, + tag, + LayoutElementCategory::Table(role), + None, + ), + ); + } + let mut base = Styles::new(); + base.hidden = false; + let styles = TableStyles { + base, + displays: HashMap::from([ + (0, LayoutDisplay::Table), + (1, LayoutDisplay::TableRowGroup), + (3, LayoutDisplay::TableRow), + (4, LayoutDisplay::TableCell), + (5, LayoutDisplay::TableCell), + ]), + aspect_ratio: None, + }; + (source, styles) +} + +#[test] +fn table_owned_graph_refreshes_styles_and_preserves_cold_paint_and_queries() { + let (mut source, mut styles) = fixture(); + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + let first = compare_with_cold(&source, &mut styles, &mut services, &mut work); + let frozen = first.tree.boxes.clone(); + let pointer = owned_parts(&work).0.as_ptr(); + let children = owned_parts(&work).0[0].children.as_ptr(); + for step in 0..3 { + styles.base.first_width = 180.0 + step as f32 * 10.0; + styles.base.color = PaintColor::new(0.25, 0.5, 0.75, 1.0); + styles.aspect_ratio = Some(1.0 + step as f32); + source.selection = Some(LayoutTextSelection::new(1, 5)); + source.label_revision = step + 1; + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_owned_boxes, work.stats().retained_boxes); + assert_eq!(owned_parts(&work).0.as_ptr(), pointer); + assert_eq!(owned_parts(&work).0[0].children.as_ptr(), children); + for layout_box in owned_parts(&work).0 { + if layout_box.kind.is_internal_table_box() { + assert!(layout_box.style.taffy.aspect_ratio.is_none()); + } + } + assert_eq!(first.tree.boxes, frozen); + } +} + +#[test] +fn table_semantic_spans_and_source_order_rebuild_before_warming_again() { + for change in 0..3 { + let (mut source, mut styles) = fixture(); + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + owned_parts(&work); + match change { + 0 => { + source + .semantics + .get_mut(&4) + .unwrap() + .metadata + .table + .as_mut() + .unwrap() + .column_span = 2 + } + 1 => source.nodes[3].children.swap(0, 1), + 2 => { + styles.displays.insert(1, LayoutDisplay::TableHeaderGroup); + } + _ => unreachable!(), + } + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_owned_boxes, 0); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert!(work.stats().reused_owned_boxes > 0); + } +} + +#[test] +fn table_anonymous_repair_keeps_using_the_shared_cold_constructor() { + for malformed in [false, true] { + let (mut source, mut styles) = fixture(); + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + if malformed { + styles.displays.insert(4, LayoutDisplay::Block); + } else { + source.nodes.push(Node::text("needs an anonymous cell")); + source.nodes[3].children.insert(0, 7); + } + for _ in 0..2 { + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().retained_owned_boxes, 0); + assert_eq!(work.stats().reused_construction_boxes, 0); + } + } +} + +#[test] +fn table_compact_admission_and_eviction_preserve_complete_output() { + let (source, mut styles) = fixture(); + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + owned_parts(&work); + let owned = work.stats().estimated_retained_bytes; + let mut retained = work.take().unwrap(); + retained.compact_graph(); + let compact = retained.estimated_retention().total(); + drop(retained); + let mut work = LayoutWorkingSet::new(LayoutWorkingSetLimits { + max_estimated_retained_bytes: compact + (owned - compact) / 2, + ..Default::default() + }); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().retained_owned_boxes, 0); + styles.base.first_width = 170.0; + let warm = compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert!(work.stats().reused_construction_boxes > 0); + assert_eq!(work.stats().reused_owned_boxes, 0); + work.clear(); + let cold = compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_construction_boxes, 0); + assert_eq!(warm.tree.boxes, cold.tree.boxes); + assert_eq!(warm.paint_snapshot(), cold.paint_snapshot()); +} + +#[test] +fn table_owned_refresh_normalizes_internal_styles_before_numeric_layout() { + let (source, mut styles) = fixture(); + styles.aspect_ratio = Some(1.0); + let world = build_layout_world_with_retained_inputs(&source, &mut styles, None).unwrap(); + for layout_box in &world.boxes { + if layout_box.kind.is_internal_table_box() { + assert_eq!(layout_box.style.taffy.aspect_ratio, None); + } + } + let mut work = LayoutWorkingSet::default(); + publish(world, &mut work); + owned_parts(&work); + styles.aspect_ratio = Some(2.0); + let mut previous = work.take(); + let world = + build_layout_world_with_retained_inputs(&source, &mut styles, previous.as_mut()).unwrap(); + assert_eq!( + world + .construction_inputs + .as_ref() + .unwrap() + .reused_owned_boxes(), + world.len() + ); + assert_eq!( + world.boxes[world.root.index()].style.taffy.aspect_ratio, + Some(2.0) + ); + for layout_box in &world.boxes { + if layout_box.kind.is_internal_table_box() { + assert_eq!( + layout_box.style.taffy.aspect_ratio, None, + "{:?}", + layout_box.kind + ); + } + } +} + +#[test] +fn table_caption_columns_and_visual_sections_match_the_shared_cold_layout() { + let (mut source, mut styles) = fixture(); + source.nodes.extend([ + Node::element(vec![8]), + Node::text("caption"), + Node::element(vec![10]), + Node::element(vec![19]), + Node::element(vec![12]), + Node::element(vec![13]), + Node::element(vec![14]), + Node::text("footer"), + Node::element(vec![16]), + Node::element(vec![17]), + Node::element(vec![18]), + Node::text("header"), + Node::text("ignored column descendant"), + ]); + source.nodes[0].children = vec![7, 9, 11, 1, 15]; + for (node, tag, role, display) in [ + ( + 7, + "caption", + LayoutTableRole::Caption, + LayoutDisplay::TableCaption, + ), + ( + 9, + "colgroup", + LayoutTableRole::ColumnGroup, + LayoutDisplay::TableColumnGroup, + ), + ( + 10, + "col", + LayoutTableRole::Column, + LayoutDisplay::TableColumn, + ), + ( + 11, + "tfoot", + LayoutTableRole::FooterGroup, + LayoutDisplay::TableFooterGroup, + ), + (12, "tr", LayoutTableRole::Row, LayoutDisplay::TableRow), + (13, "td", LayoutTableRole::Cell, LayoutDisplay::TableCell), + ( + 15, + "thead", + LayoutTableRole::HeaderGroup, + LayoutDisplay::TableHeaderGroup, + ), + (16, "tr", LayoutTableRole::Row, LayoutDisplay::TableRow), + (17, "td", LayoutTableRole::Cell, LayoutDisplay::TableCell), + ] { + source.semantics.insert( + node, + LayoutElementSemantics::new( + LayoutNamespace::Html, + tag, + LayoutElementCategory::Table(role), + None, + ), + ); + styles.displays.insert(node, display); + } + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + owned_parts(&work); + // The shared constructor never consumes column descendants. + source.nodes[19].text = Some("still ignored".into()); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_owned_boxes, work.stats().retained_boxes); + styles.displays.insert(0, LayoutDisplay::InlineTable); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_owned_boxes, 0); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert!(work.stats().reused_owned_boxes > 0); + source + .semantics + .get_mut(&10) + .unwrap() + .metadata + .table + .as_mut() + .unwrap() + .span = 2; + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_owned_boxes, 0); +} + +#[test] +fn table_discarded_column_group_elements_do_not_enter_the_retained_style_sequence() { + let (mut source, mut styles) = fixture(); + source.nodes.extend([ + Node::element(vec![8]), + Node::element(vec![9]), + Node::text("discarded"), + ]); + source.nodes[0].children.insert(0, 7); + source.semantics.insert( + 7, + LayoutElementSemantics::new( + LayoutNamespace::Html, + "colgroup", + LayoutElementCategory::Table(LayoutTableRole::ColumnGroup), + None, + ), + ); + styles.displays.insert(7, LayoutDisplay::TableColumnGroup); + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + for _ in 0..2 { + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_construction_boxes, 0); + assert_eq!(work.stats().retained_owned_boxes, 0); + } +} + +#[test] +fn table_inline_wrapper_inside_an_atomic_parent_keeps_current_geometry() { + let (mut source, mut styles) = fixture(); + source.nodes[0].children = vec![7]; + source.nodes.push(Node::element(vec![1])); + source.semantics.remove(&0); + source.semantics.insert( + 7, + LayoutElementSemantics::new( + LayoutNamespace::Html, + "table", + LayoutElementCategory::Table(LayoutTableRole::Table), + None, + ), + ); + styles.displays.insert(0, LayoutDisplay::Block); + styles.displays.insert(7, LayoutDisplay::InlineTable); + let mut services = DocumentLayoutServices::new(); + let mut work = LayoutWorkingSet::default(); + compare_with_cold(&source, &mut styles, &mut services, &mut work); + owned_parts(&work); + styles.base.first_width = 200.0; + compare_with_cold(&source, &mut styles, &mut services, &mut work); + assert_eq!(work.stats().reused_owned_boxes, work.stats().retained_boxes); +} diff --git a/moli-layout/src/world.rs b/moli-layout/src/world.rs index 5c846b6c22..ca97151984 100644 --- a/moli-layout/src/world.rs +++ b/moli-layout/src/world.rs @@ -1233,7 +1233,8 @@ where Ok(()) } - pub(crate) fn refresh_box_style(layout_box: &mut LayoutBox, style: ResolvedLayoutStyle) { + pub(crate) fn refresh_box_style(layout_box: &mut LayoutBox, mut style: ResolvedLayoutStyle) { + Self::normalize_box_style(layout_box.kind, &mut style); layout_box.capability_diagnostics = default_capability_diagnostics( layout_box.kind, layout_box.element_semantics.as_ref(), @@ -1242,6 +1243,16 @@ where layout_box.style = style; } + fn normalize_box_style(kind: LayoutBoxKind, style: &mut ResolvedLayoutStyle) { + if kind.is_internal_table_box() { + // Keep the authored/computed value in `preferred_aspect_ratio`. + // The table formatter supplies internal track and cell sizes; + // construction and retained refresh must suppress the same + // numeric projection before generic sizing consumes it. + style.taffy.aspect_ratio = None; + } + } + pub(crate) fn new_box( source: Option, owner: Option, @@ -1254,15 +1265,7 @@ where mut style: ResolvedLayoutStyle, text: Option>, ) -> LayoutBox { - if kind.is_internal_table_box() { - // Keep the authored/computed value in `preferred_aspect_ratio`. - // Only the numeric backend projection is suppressed: CSS Sizing - // excludes internal table boxes from `aspect-ratio`, and the table - // formatter supplies their used track and cell sizes. Chromium - // represents the same boundary with its table-cell constraint - // space before generic block-size resolution. - style.taffy.aspect_ratio = None; - } + Self::normalize_box_style(kind, &mut style); let capability_diagnostics = default_capability_diagnostics(kind, element_semantics.as_ref(), &style); LayoutBox { diff --git a/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs b/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs index af7537869d..99b6e13ef8 100644 --- a/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs +++ b/moli-renderer-v8/src/script_vm/tests/realtime_working_sets/table.rs @@ -50,6 +50,7 @@ fn realtime_table_reuses_unchanged_text_results_within_the_default_budget() { let initial = vm.eval(READ).unwrap(); let cold = main_stats(&vm).unwrap(); assert!(cold.retained_boxes >= 2 * ITEMS, "{cold:?}"); + assert!(cold.estimated_construction_bytes > 0, "{cold:?}"); assert!(cold.estimated_retained_bytes <= moli_layout::DEFAULT_DOCUMENT_WORKING_SET_BYTES); let builds = vm.layout_pass_observability_for_test().1; vm.eval("sheet.querySelector('.item').style.width='333px'") @@ -57,6 +58,15 @@ fn realtime_table_reuses_unchanged_text_results_within_the_default_budget() { let updated = vm.eval(READ).unwrap(); assert_ne!(initial, updated); let warm = main_stats(&vm).unwrap(); + assert_eq!( + warm.reused_construction_boxes, warm.retained_boxes, + "{warm:?}" + ); + assert_eq!( + warm.reused_owned_boxes, cold.retained_owned_boxes, + "{warm:?}" + ); + assert_eq!(warm.retained_boxes, cold.retained_boxes, "{warm:?}"); assert!(warm.reused_inline_contexts >= ITEMS - 1, "{warm:?}"); assert!(warm.reused_inline_numeric_layouts >= ITEMS - 1, "{warm:?}"); assert!(warm.reused_inline_measurements >= ITEMS - 1, "{warm:?}"); @@ -80,10 +90,49 @@ fn realtime_table_reuses_unchanged_text_results_within_the_default_budget() { "{stable_constraints:?}" ); assert_eq!(vm.layout_pass_observability_for_test().1, builds + 2); + let capture = |vm: &mut StandaloneScriptVmHarness| { + let screen = viewport(vm); + vm.with_fresh_layout_pass( + LayoutPassRequest::with_paint(screen, LayoutFlushReason::Test), + LayoutPublicationRequest::Screen, + |pass| Ok(pass.paint_snapshot().cloned()), + ) + .unwrap() + .unwrap() + .unwrap() + }; + let warm_paint = capture(&mut vm); force_cold(&mut vm); assert_eq!(vm.eval(READ).unwrap(), updated); assert_eq!(main_stats(&vm).unwrap().reused_inline_numeric_layouts, 0); assert_eq!(main_stats(&vm).unwrap().reused_inline_measurements, 0); + assert_eq!(main_stats(&vm).unwrap().reused_construction_boxes, 0); + let cold_paint = capture(&mut vm); + assert_eq!(warm_paint, cold_paint); +} + +#[test] +fn realtime_table_reuses_actual_box_storage_within_the_default_budget() { + const ITEMS: usize = 256; + let mut vm = table_fixture(ITEMS); + // A smaller real table admits the actual box storage without increasing + // the production limit. The 1024-item fixture covers compact fallback. + let initial = vm.eval(READ).unwrap(); + let cold = main_stats(&vm).unwrap(); + assert!(cold.retained_boxes >= 2 * ITEMS, "{cold:?}"); + assert_eq!(cold.retained_owned_boxes, cold.retained_boxes, "{cold:?}"); + assert!(cold.estimated_retained_bytes <= moli_layout::DEFAULT_DOCUMENT_WORKING_SET_BYTES); + vm.eval("sheet.querySelector('.item').style.width='333px'") + .unwrap(); + let updated = vm.eval(READ).unwrap(); + assert_ne!(initial, updated); + let warm = main_stats(&vm).unwrap(); + assert_eq!(warm.reused_owned_boxes, cold.retained_boxes, "{warm:?}"); + assert_eq!(warm.retained_owned_boxes, warm.retained_boxes, "{warm:?}"); + assert!(warm.reused_inline_measurements >= ITEMS - 1, "{warm:?}"); + assert!(warm.reused_inline_numeric_layouts >= ITEMS - 1, "{warm:?}"); + force_cold(&mut vm); + assert_eq!(vm.eval(READ).unwrap(), updated); } #[test]