mirror of
https://github.com/lexmount/moli.git
synced 2026-11-13 08:08:10 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Node>,
|
||||
semantics: HashMap<usize, LayoutElementSemantics>,
|
||||
hidden_name: Arc<str>,
|
||||
scroll: LayoutPoint,
|
||||
selection: Option<LayoutTextSelection>,
|
||||
@@ -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<LayoutElementSemantics> {
|
||||
(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 {
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
use super::*;
|
||||
use crate::LayoutTableRole;
|
||||
|
||||
struct TableStyles {
|
||||
base: Styles,
|
||||
displays: HashMap<usize, LayoutDisplay>,
|
||||
aspect_ratio: Option<f32>,
|
||||
}
|
||||
|
||||
impl LayoutStyleResolver<usize> for TableStyles {
|
||||
fn element_styles(
|
||||
&mut self,
|
||||
node: usize,
|
||||
) -> Result<Option<ResolvedLayoutElementStyles>, 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);
|
||||
}
|
||||
+13
-10
@@ -1233,7 +1233,8 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_box_style(layout_box: &mut LayoutBox<N>, style: ResolvedLayoutStyle) {
|
||||
pub(crate) fn refresh_box_style(layout_box: &mut LayoutBox<N>, 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<N>,
|
||||
owner: Option<N>,
|
||||
@@ -1254,15 +1265,7 @@ where
|
||||
mut style: ResolvedLayoutStyle,
|
||||
text: Option<Arc<str>>,
|
||||
) -> LayoutBox<N> {
|
||||
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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user