Retain exact source changes and immutable use topology
This commit is contained in:
parent
826f195236
commit
220f27803e
1441
native/src/layout.rs
1441
native/src/layout.rs
File diff suppressed because it is too large
Load Diff
@ -484,6 +484,7 @@ struct DocumentInputStats {
|
|||||||
delta_entries_parsed: u64,
|
delta_entries_parsed: u64,
|
||||||
delta_entries_validated: u64,
|
delta_entries_validated: u64,
|
||||||
trie_path_nodes_copied: u64,
|
trie_path_nodes_copied: u64,
|
||||||
|
source_change_work: layout::SourceChangeWork,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -523,6 +524,7 @@ struct RuntimeState {
|
|||||||
document_delta_entries_validated: u64,
|
document_delta_entries_validated: u64,
|
||||||
document_trie_path_nodes_copied: u64,
|
document_trie_path_nodes_copied: u64,
|
||||||
document_resolver_lookups: u64,
|
document_resolver_lookups: u64,
|
||||||
|
source_change_work: layout::SourceChangeWork,
|
||||||
atom_plan_work: layout::AtomPlanWork,
|
atom_plan_work: layout::AtomPlanWork,
|
||||||
line_plan_work: layout::LinePlanWork,
|
line_plan_work: layout::LinePlanWork,
|
||||||
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
|
||||||
@ -602,6 +604,7 @@ struct SessionStats {
|
|||||||
document_delta_entries_validated: u64,
|
document_delta_entries_validated: u64,
|
||||||
document_trie_path_nodes_copied: u64,
|
document_trie_path_nodes_copied: u64,
|
||||||
document_resolver_lookups: u64,
|
document_resolver_lookups: u64,
|
||||||
|
source_change_work: layout::SourceChangeWork,
|
||||||
atom_plan_work: layout::AtomPlanWork,
|
atom_plan_work: layout::AtomPlanWork,
|
||||||
line_plan_work: layout::LinePlanWork,
|
line_plan_work: layout::LinePlanWork,
|
||||||
pending_baselines: usize,
|
pending_baselines: usize,
|
||||||
@ -676,6 +679,7 @@ impl Session {
|
|||||||
document_delta_entries_validated: 0,
|
document_delta_entries_validated: 0,
|
||||||
document_trie_path_nodes_copied: 0,
|
document_trie_path_nodes_copied: 0,
|
||||||
document_resolver_lookups: 0,
|
document_resolver_lookups: 0,
|
||||||
|
source_change_work: layout::SourceChangeWork::default(),
|
||||||
atom_plan_work: layout::AtomPlanWork::default(),
|
atom_plan_work: layout::AtomPlanWork::default(),
|
||||||
line_plan_work: layout::LinePlanWork::default(),
|
line_plan_work: layout::LinePlanWork::default(),
|
||||||
confirmed_baseline,
|
confirmed_baseline,
|
||||||
@ -1019,14 +1023,38 @@ impl Session {
|
|||||||
.to_owned(),
|
.to_owned(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
let (document, parsed, copied) = document.apply_delta(delta)?;
|
let applied = document.apply_delta(delta)?;
|
||||||
|
if !applied.changes.applies_to(document, &applied.document) {
|
||||||
|
return Err(
|
||||||
|
"Native retained source changes document identity mismatch".to_owned()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Consume exact source facts for input accounting. The renderer
|
||||||
|
// still evaluates the target normally; no history-bearing change
|
||||||
|
// object is stored in a job, document or confirmed baseline.
|
||||||
|
let mut source_change_work = applied.stats.source_work;
|
||||||
|
let mut previous_owner = None;
|
||||||
|
applied
|
||||||
|
.changes
|
||||||
|
.visit_changed_slots(|owner, _, _, _, fields| {
|
||||||
|
if previous_owner != Some(owner) {
|
||||||
|
source_change_work.owners_changed += 1;
|
||||||
|
previous_owner = Some(owner);
|
||||||
|
}
|
||||||
|
source_change_work.slots_changed += 1;
|
||||||
|
source_change_work.fields_changed += fields.len() as u64;
|
||||||
|
});
|
||||||
|
let (styles, property_templates) = applied.changes.registry_ranges();
|
||||||
|
source_change_work.styles_appended = styles.len() as u64;
|
||||||
|
source_change_work.property_templates_added = property_templates.len() as u64;
|
||||||
(
|
(
|
||||||
LayoutSource::Retained(document),
|
LayoutSource::Retained(applied.document),
|
||||||
DocumentInputStats {
|
DocumentInputStats {
|
||||||
delta_input_bytes: payload.len() as u64,
|
delta_input_bytes: payload.len() as u64,
|
||||||
delta_entries_parsed: parsed,
|
delta_entries_parsed: applied.stats.entries_parsed,
|
||||||
delta_entries_validated: parsed,
|
delta_entries_validated: applied.stats.entries_parsed,
|
||||||
trie_path_nodes_copied: copied,
|
trie_path_nodes_copied: applied.stats.trie_path_nodes_copied,
|
||||||
|
source_change_work,
|
||||||
..DocumentInputStats::default()
|
..DocumentInputStats::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1113,6 +1141,9 @@ impl Session {
|
|||||||
state.document_delta_entries_parsed += input_stats.delta_entries_parsed;
|
state.document_delta_entries_parsed += input_stats.delta_entries_parsed;
|
||||||
state.document_delta_entries_validated += input_stats.delta_entries_validated;
|
state.document_delta_entries_validated += input_stats.delta_entries_validated;
|
||||||
state.document_trie_path_nodes_copied += input_stats.trie_path_nodes_copied;
|
state.document_trie_path_nodes_copied += input_stats.trie_path_nodes_copied;
|
||||||
|
state
|
||||||
|
.source_change_work
|
||||||
|
.accumulate(input_stats.source_change_work);
|
||||||
state.pending_baselines.clear();
|
state.pending_baselines.clear();
|
||||||
if let Some(pending) = output.pending {
|
if let Some(pending) = output.pending {
|
||||||
state
|
state
|
||||||
@ -1252,6 +1283,7 @@ impl Session {
|
|||||||
document_delta_entries_validated: state.document_delta_entries_validated,
|
document_delta_entries_validated: state.document_delta_entries_validated,
|
||||||
document_trie_path_nodes_copied: state.document_trie_path_nodes_copied,
|
document_trie_path_nodes_copied: state.document_trie_path_nodes_copied,
|
||||||
document_resolver_lookups: state.document_resolver_lookups,
|
document_resolver_lookups: state.document_resolver_lookups,
|
||||||
|
source_change_work: state.source_change_work,
|
||||||
atom_plan_work: state.atom_plan_work,
|
atom_plan_work: state.atom_plan_work,
|
||||||
line_plan_work: state.line_plan_work,
|
line_plan_work: state.line_plan_work,
|
||||||
pending_baselines,
|
pending_baselines,
|
||||||
@ -2677,6 +2709,14 @@ mod tests {
|
|||||||
assert_eq!(stats.document_delta_entries_validated, 1);
|
assert_eq!(stats.document_delta_entries_validated, 1);
|
||||||
assert_eq!(stats.document_trie_path_nodes_copied, 17);
|
assert_eq!(stats.document_trie_path_nodes_copied, 17);
|
||||||
assert_eq!(stats.document_delta_input_bytes, delta.len() as u64);
|
assert_eq!(stats.document_delta_input_bytes, delta.len() as u64);
|
||||||
|
assert_eq!(stats.source_change_work.fields_compared, 1);
|
||||||
|
assert_eq!(stats.source_change_work.text_lines_compared, 1);
|
||||||
|
assert_eq!(stats.source_change_work.text_clusters_compared, 1);
|
||||||
|
assert_eq!(stats.source_change_work.text_bytes_compared, 1);
|
||||||
|
assert_eq!(stats.source_change_work.local_nodes_copied, 1);
|
||||||
|
assert_eq!(stats.source_change_work.owners_changed, 1);
|
||||||
|
assert_eq!(stats.source_change_work.slots_changed, 1);
|
||||||
|
assert_eq!(stats.source_change_work.fields_changed, 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stats.document_resolver_lookups, stats.target_renders,
|
stats.document_resolver_lookups, stats.target_renders,
|
||||||
"each single-owner render resolves its root; retained context validation resolves no nodes"
|
"each single-owner render resolves its root; retained context validation resolves no nodes"
|
||||||
@ -2693,6 +2733,60 @@ mod tests {
|
|||||||
child.stop(true);
|
child.stop(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_source_input_counts_noops_and_registries_without_publishing_failed_work() {
|
||||||
|
let session = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||||
|
let first = identified_proof_layout_payload(
|
||||||
|
r#"[{"key":1,"viewport-width":80,"viewport-height":10,"root-width":80,"runtime-revision":0,"context-hash":77}]"#,
|
||||||
|
);
|
||||||
|
session.render_sync(1, &first).unwrap();
|
||||||
|
assert!(session.confirm(1, 1, 1).unwrap());
|
||||||
|
let document: serde_json::Value = serde_json::from_slice(&first).unwrap();
|
||||||
|
let content = document["document"]["root"]["content"].clone();
|
||||||
|
let payload = |padding| {
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"version": 1, "document-base-revision": 1, "document-target-revision": 2,
|
||||||
|
"document-delta": {
|
||||||
|
"style-base-count": 0, "styles-append": [{"mode": "add", "face": {"foreground": "red"}}],
|
||||||
|
"property-template-base-count": 0, "property-template-target-count": 2,
|
||||||
|
"entries": [{"node-id": 1, "expected-revision": 7, "target-revision": 8,
|
||||||
|
"slot-patches": [{"slot": 0, "local": {"content": content, "padding-left": padding}}]}]
|
||||||
|
},
|
||||||
|
"frames": [{"key": 2, "viewport-width": 80, "viewport-height": 10,
|
||||||
|
"root-width": 80, "runtime-revision": 1, "context-hash": 77}]
|
||||||
|
})).unwrap()
|
||||||
|
};
|
||||||
|
let before = session.stats().source_change_work;
|
||||||
|
assert!(session.render_sync(2, &payload(-1)).is_err());
|
||||||
|
assert_eq!(session.stats().source_change_work, before);
|
||||||
|
session.render_sync(3, &payload(0)).unwrap();
|
||||||
|
let work = session.stats().source_change_work;
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
work.fields_compared,
|
||||||
|
work.text_lines_compared,
|
||||||
|
work.text_clusters_compared,
|
||||||
|
work.text_bytes_compared
|
||||||
|
),
|
||||||
|
(2, 1, 1, 1)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
work.local_nodes_copied,
|
||||||
|
work.owners_changed,
|
||||||
|
work.slots_changed,
|
||||||
|
work.fields_changed
|
||||||
|
),
|
||||||
|
(0, 0, 0, 0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(work.styles_appended, work.property_templates_added),
|
||||||
|
(1, 2)
|
||||||
|
);
|
||||||
|
assert!(session.confirm(3, 2, 2).unwrap());
|
||||||
|
session.stop(true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
|
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
|
||||||
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
|
||||||
|
|||||||
214
native/src/source_topology.rs
Normal file
214
native/src/source_topology.rs
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
//! Immutable source-reference occurrences, independent of layout evaluation phases.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) enum LocalStep {
|
||||||
|
BoxChild,
|
||||||
|
RowChild(usize),
|
||||||
|
ColumnChild(usize),
|
||||||
|
FlexItem(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A local structural address stops before crossing an identified node reference.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) struct SourceAddress {
|
||||||
|
pub(super) owner_id: u64,
|
||||||
|
pub(super) path: Arc<[LocalStep]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One structural occurrence can have several distinct layout evaluation uses.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(super) struct ParentUse {
|
||||||
|
pub(super) use_id: u64,
|
||||||
|
pub(super) parent: SourceAddress,
|
||||||
|
pub(super) child_slot: LocalStep,
|
||||||
|
pub(super) target_owner: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicit bootstrap work; excludes allocator/map internals and layout work/K.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub(super) struct TopologyBuildWork {
|
||||||
|
pub(super) occurrences_written: u64,
|
||||||
|
pub(super) path_steps_copied: u64,
|
||||||
|
pub(super) incoming_lists_frozen: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct SourceTopology {
|
||||||
|
root_owner: u64,
|
||||||
|
incoming: BTreeMap<u64, Box<[ParentUse]>>,
|
||||||
|
build_work: TopologyBuildWork,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceTopology {
|
||||||
|
pub(super) fn root_owner(&self) -> u64 {
|
||||||
|
self.root_owner
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrows the complete occurrence list; no owner-pair deduplication.
|
||||||
|
pub(super) fn incoming_uses(&self, owner_id: u64) -> &[ParentUse] {
|
||||||
|
self.incoming.get(&owner_id).map_or(&[], Box::as_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn build_work(&self) -> TopologyBuildWork {
|
||||||
|
self.build_work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Only the bootstrap traversal can create a topology. Scalar deltas share it.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct TopologyBuilder {
|
||||||
|
incoming: BTreeMap<u64, Vec<ParentUse>>,
|
||||||
|
work: TopologyBuildWork,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TopologyBuilder {
|
||||||
|
pub(super) fn add_use(
|
||||||
|
&mut self,
|
||||||
|
parent_owner: u64,
|
||||||
|
parent_path: &[LocalStep],
|
||||||
|
child_slot: LocalStep,
|
||||||
|
target_owner: u64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let use_id = self.work.occurrences_written;
|
||||||
|
let occurrences_written = use_id
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| "Native source topology occurrence count overflow".to_owned())?;
|
||||||
|
let path_steps_copied = self
|
||||||
|
.work
|
||||||
|
.path_steps_copied
|
||||||
|
.checked_add(parent_path.len() as u64)
|
||||||
|
.ok_or_else(|| "Native source topology path count overflow".to_owned())?;
|
||||||
|
self.incoming
|
||||||
|
.entry(target_owner)
|
||||||
|
.or_default()
|
||||||
|
.push(ParentUse {
|
||||||
|
use_id,
|
||||||
|
parent: SourceAddress {
|
||||||
|
owner_id: parent_owner,
|
||||||
|
path: Arc::from(parent_path),
|
||||||
|
},
|
||||||
|
child_slot,
|
||||||
|
target_owner,
|
||||||
|
});
|
||||||
|
self.work.occurrences_written = occurrences_written;
|
||||||
|
self.work.path_steps_copied = path_steps_copied;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The caller has already validated owner uniqueness and rejected NodeRef input.
|
||||||
|
pub(super) fn finish(self, root_owner: u64) -> Arc<SourceTopology> {
|
||||||
|
let mut work = self.work;
|
||||||
|
let incoming = self
|
||||||
|
.incoming
|
||||||
|
.into_iter()
|
||||||
|
.map(|(owner, uses)| {
|
||||||
|
work.incoming_lists_frozen += 1;
|
||||||
|
(owner, uses.into_boxed_slice())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Arc::new(SourceTopology {
|
||||||
|
root_owner,
|
||||||
|
incoming,
|
||||||
|
build_work: work,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anonymous_paths_and_structural_slots_remain_distinct() {
|
||||||
|
let mut builder = TopologyBuilder::default();
|
||||||
|
builder
|
||||||
|
.add_use(11, &[LocalStep::BoxChild], LocalStep::FlexItem(3), 29)
|
||||||
|
.unwrap();
|
||||||
|
builder
|
||||||
|
.add_use(
|
||||||
|
29,
|
||||||
|
&[LocalStep::BoxChild, LocalStep::RowChild(2)],
|
||||||
|
LocalStep::ColumnChild(1),
|
||||||
|
41,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let topology = builder.finish(11);
|
||||||
|
assert_eq!(topology.root_owner(), 11);
|
||||||
|
assert!(topology.incoming_uses(11).is_empty());
|
||||||
|
assert!(topology.incoming_uses(999).is_empty());
|
||||||
|
let first = &topology.incoming_uses(29)[0];
|
||||||
|
assert_eq!(first.use_id, 0);
|
||||||
|
assert_eq!(first.parent.owner_id, 11);
|
||||||
|
assert_eq!(first.parent.path.as_ref(), &[LocalStep::BoxChild]);
|
||||||
|
assert_eq!(first.child_slot, LocalStep::FlexItem(3));
|
||||||
|
assert_eq!(first.target_owner, 29);
|
||||||
|
let second = &topology.incoming_uses(41)[0];
|
||||||
|
assert_eq!(second.parent.owner_id, 29);
|
||||||
|
assert_eq!(second.use_id, 1);
|
||||||
|
assert_eq!(
|
||||||
|
second.parent.path.as_ref(),
|
||||||
|
&[LocalStep::BoxChild, LocalStep::RowChild(2)]
|
||||||
|
);
|
||||||
|
assert_eq!(second.child_slot, LocalStep::ColumnChild(1));
|
||||||
|
assert_eq!(
|
||||||
|
topology.build_work(),
|
||||||
|
TopologyBuildWork {
|
||||||
|
occurrences_written: 2,
|
||||||
|
path_steps_copied: 3,
|
||||||
|
incoming_lists_frozen: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_occurrence_is_retained_even_for_the_same_owner_pair() {
|
||||||
|
// The storage must not silently deduplicate occurrences. This does not
|
||||||
|
// authorize duplicate identified owners in the public bootstrap input.
|
||||||
|
let mut builder = TopologyBuilder::default();
|
||||||
|
for slot in [2, 7, 2] {
|
||||||
|
builder
|
||||||
|
.add_use(1, &[], LocalStep::RowChild(slot), 9)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let topology = builder.finish(1);
|
||||||
|
let uses = topology.incoming_uses(9);
|
||||||
|
assert_eq!(uses.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
uses.iter().map(|usage| usage.use_id).collect::<Vec<_>>(),
|
||||||
|
[0, 1, 2]
|
||||||
|
);
|
||||||
|
assert_eq!(uses[0].child_slot, uses[2].child_slot);
|
||||||
|
assert_ne!(uses[0].use_id, uses[2].use_id);
|
||||||
|
assert_eq!(topology.build_work().incoming_lists_frozen, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forks_borrow_the_same_lists_without_changing_build_work() {
|
||||||
|
for n in [32, 128, 512] {
|
||||||
|
let mut builder = TopologyBuilder::default();
|
||||||
|
for index in 0..n {
|
||||||
|
builder
|
||||||
|
.add_use(1, &[], LocalStep::ColumnChild(index), index as u64 + 2)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let base = builder.finish(1);
|
||||||
|
let original_work = base.build_work();
|
||||||
|
let left = Arc::clone(&base);
|
||||||
|
let right = Arc::clone(&base);
|
||||||
|
let target = (n / 2 + 2) as u64;
|
||||||
|
assert!(Arc::ptr_eq(&base, &left));
|
||||||
|
assert!(Arc::ptr_eq(&left, &right));
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
base.incoming_uses(target),
|
||||||
|
right.incoming_uses(target)
|
||||||
|
));
|
||||||
|
assert_eq!(base.build_work(), original_work);
|
||||||
|
assert_eq!(original_work.occurrences_written, n as u64);
|
||||||
|
assert_eq!(original_work.path_steps_copied, 0);
|
||||||
|
assert_eq!(original_work.incoming_lists_frozen, n as u64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
native/src/source_topology_tests.rs
Normal file
162
native/src/source_topology_tests.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Included in layout::tests after the production bootstrap topology is attached.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_topology_bootstrap_records_exact_anonymous_use_paths() {
|
||||||
|
use source_topology::LocalStep;
|
||||||
|
let leaf = |id| {
|
||||||
|
identified(
|
||||||
|
text_box(
|
||||||
|
id as i64,
|
||||||
|
measured_text(vec![vec![cluster("x", 1, None)]]),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
id,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let nested_owner = identified(child_box(3, child_box(30, leaf(4), None), None), 3, 1);
|
||||||
|
let root = identified(
|
||||||
|
child_box(
|
||||||
|
1,
|
||||||
|
LayoutNode::Column {
|
||||||
|
node_id: None,
|
||||||
|
node_revision: None,
|
||||||
|
children: Arc::new(vec![
|
||||||
|
LayoutNode::Row {
|
||||||
|
node_id: None,
|
||||||
|
node_revision: None,
|
||||||
|
children: Arc::new(vec![
|
||||||
|
text_box(20, measured_text(vec![vec![cluster("a", 1, None)]]), None),
|
||||||
|
leaf(2),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
context_test_flex(Size::Auto, vec![nested_owner]),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let full = retained_document(root);
|
||||||
|
let (retained, _) = RetainedDocument::bootstrap(full.clone()).unwrap();
|
||||||
|
let topology = &retained.topology;
|
||||||
|
assert_eq!(topology.root_owner(), 1);
|
||||||
|
assert!(topology.incoming_uses(1).is_empty());
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
vec![LocalStep::BoxChild, LocalStep::ColumnChild(0)],
|
||||||
|
LocalStep::RowChild(1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
vec![LocalStep::BoxChild, LocalStep::ColumnChild(1)],
|
||||||
|
LocalStep::FlexItem(0),
|
||||||
|
),
|
||||||
|
(4, 3, vec![LocalStep::BoxChild], LocalStep::BoxChild),
|
||||||
|
];
|
||||||
|
let mut use_ids = HashSet::new();
|
||||||
|
for (target, parent, path, slot) in cases {
|
||||||
|
let uses = topology.incoming_uses(target);
|
||||||
|
assert_eq!(uses.len(), 1);
|
||||||
|
assert_eq!(uses[0].target_owner, target);
|
||||||
|
assert_eq!(uses[0].parent.owner_id, parent);
|
||||||
|
assert_eq!(uses[0].parent.path.as_ref(), path.as_slice());
|
||||||
|
assert_eq!(uses[0].child_slot, slot);
|
||||||
|
assert!(use_ids.insert(uses[0].use_id));
|
||||||
|
}
|
||||||
|
assert_eq!(topology.build_work().occurrences_written, 3);
|
||||||
|
assert_eq!(topology.build_work().path_steps_copied, 5);
|
||||||
|
assert_eq!(
|
||||||
|
retained.layout_tape(test_context(), None).unwrap(),
|
||||||
|
full.layout_tape(test_context(), None).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_topology_real_deltas_share_lists_across_forks_and_failures() {
|
||||||
|
for n in [32, 128, 512] {
|
||||||
|
let children = (0..n)
|
||||||
|
.map(|i| {
|
||||||
|
identified(
|
||||||
|
text_box(
|
||||||
|
i + 2,
|
||||||
|
measured_text(vec![vec![cluster("x", 1, None)]]),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(i + 2) as u64,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let root = identified(
|
||||||
|
LayoutNode::Column {
|
||||||
|
node_id: None,
|
||||||
|
node_revision: None,
|
||||||
|
children: Arc::new(children),
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let (base, _) = RetainedDocument::bootstrap(retained_document(root)).unwrap();
|
||||||
|
let target_id = (n / 2 + 2) as u64;
|
||||||
|
let original = Arc::clone(&base.topology);
|
||||||
|
let original_list = original.incoming_uses(target_id);
|
||||||
|
let patch = |region| {
|
||||||
|
context_test_delta(vec![serde_json::json!({
|
||||||
|
"node-id": target_id, "expected-revision": 1, "target-revision": 2,
|
||||||
|
"slot-patches": [{"slot": 0, "local": {"region-id": region}}]
|
||||||
|
})])
|
||||||
|
};
|
||||||
|
let left = base.apply_delta(patch(100_000)).unwrap().document;
|
||||||
|
let right = base.apply_delta(patch(100_001)).unwrap().document;
|
||||||
|
for target in [&base, &left, &right] {
|
||||||
|
assert!(Arc::ptr_eq(&original, &target.topology));
|
||||||
|
assert!(std::ptr::eq(
|
||||||
|
original_list,
|
||||||
|
target.topology.incoming_uses(target_id)
|
||||||
|
));
|
||||||
|
assert_eq!(target.topology.root_owner(), 1);
|
||||||
|
}
|
||||||
|
assert_eq!(original.incoming_uses(target_id)[0].target_owner, target_id);
|
||||||
|
assert_eq!(original.build_work().occurrences_written, n as u64);
|
||||||
|
// Later-entry rejection cannot install a partially updated topology.
|
||||||
|
let failed = base.apply_delta(context_test_delta(vec![
|
||||||
|
serde_json::json!({"node-id": target_id, "expected-revision": 1, "target-revision": 2}),
|
||||||
|
serde_json::json!({"node-id": 999_999, "expected-revision": 1, "target-revision": 2}),
|
||||||
|
]));
|
||||||
|
assert!(failed.is_err());
|
||||||
|
assert!(Arc::ptr_eq(&base.topology, &original));
|
||||||
|
assert_eq!(radix_lookup(&base.entries, target_id).unwrap().revision, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_topology_identity_is_local_to_one_validated_bootstrap() {
|
||||||
|
let leaf = identified(
|
||||||
|
text_box(1, measured_text(vec![vec![cluster("x", 1, None)]]), None),
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
let full = retained_document(leaf.clone());
|
||||||
|
let (first, _) = RetainedDocument::bootstrap(full.clone()).unwrap();
|
||||||
|
let (second, _) = RetainedDocument::bootstrap(full).unwrap();
|
||||||
|
assert!(!Arc::ptr_eq(&first.topology, &second.topology));
|
||||||
|
assert!(first.topology.incoming_uses(1).is_empty());
|
||||||
|
assert_eq!(first.topology.build_work().occurrences_written, 0);
|
||||||
|
let duplicate = identified(
|
||||||
|
LayoutNode::Row {
|
||||||
|
node_id: None,
|
||||||
|
node_revision: None,
|
||||||
|
children: Arc::new(vec![leaf.clone(), leaf]),
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
assert!(RetainedDocument::bootstrap(retained_document(duplicate)).is_err());
|
||||||
|
let external = identified(child_box(2, LayoutNode::NodeRef { node_id: 1 }, None), 2, 1);
|
||||||
|
assert!(RetainedDocument::bootstrap(retained_document(external)).is_err());
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user