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:
ldm0
2026-11-13 08:14:49 +08:00
parent 5b5594a2df
commit cd53532371
7 changed files with 507 additions and 33 deletions
@@ -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;
+9 -7
View File
@@ -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
View File
@@ -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]