Retain exact source changes and immutable use topology

This commit is contained in:
Kinneyzhang 2026-09-05 12:56:20 +08:00
parent 826f195236
commit 220f27803e
4 changed files with 1628 additions and 293 deletions

File diff suppressed because it is too large Load Diff

View File

@ -484,6 +484,7 @@ struct DocumentInputStats {
delta_entries_parsed: u64,
delta_entries_validated: u64,
trie_path_nodes_copied: u64,
source_change_work: layout::SourceChangeWork,
}
#[derive(Debug)]
@ -523,6 +524,7 @@ struct RuntimeState {
document_delta_entries_validated: u64,
document_trie_path_nodes_copied: u64,
document_resolver_lookups: u64,
source_change_work: layout::SourceChangeWork,
atom_plan_work: layout::AtomPlanWork,
line_plan_work: layout::LinePlanWork,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
@ -602,6 +604,7 @@ struct SessionStats {
document_delta_entries_validated: u64,
document_trie_path_nodes_copied: u64,
document_resolver_lookups: u64,
source_change_work: layout::SourceChangeWork,
atom_plan_work: layout::AtomPlanWork,
line_plan_work: layout::LinePlanWork,
pending_baselines: usize,
@ -676,6 +679,7 @@ impl Session {
document_delta_entries_validated: 0,
document_trie_path_nodes_copied: 0,
document_resolver_lookups: 0,
source_change_work: layout::SourceChangeWork::default(),
atom_plan_work: layout::AtomPlanWork::default(),
line_plan_work: layout::LinePlanWork::default(),
confirmed_baseline,
@ -1019,14 +1023,38 @@ impl Session {
.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 {
delta_input_bytes: payload.len() as u64,
delta_entries_parsed: parsed,
delta_entries_validated: parsed,
trie_path_nodes_copied: copied,
delta_entries_parsed: applied.stats.entries_parsed,
delta_entries_validated: applied.stats.entries_parsed,
trie_path_nodes_copied: applied.stats.trie_path_nodes_copied,
source_change_work,
..DocumentInputStats::default()
},
)
@ -1113,6 +1141,9 @@ impl Session {
state.document_delta_entries_parsed += input_stats.delta_entries_parsed;
state.document_delta_entries_validated += input_stats.delta_entries_validated;
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();
if let Some(pending) = output.pending {
state
@ -1252,6 +1283,7 @@ impl Session {
document_delta_entries_validated: state.document_delta_entries_validated,
document_trie_path_nodes_copied: state.document_trie_path_nodes_copied,
document_resolver_lookups: state.document_resolver_lookups,
source_change_work: state.source_change_work,
atom_plan_work: state.atom_plan_work,
line_plan_work: state.line_plan_work,
pending_baselines,
@ -2677,6 +2709,14 @@ mod tests {
assert_eq!(stats.document_delta_entries_validated, 1);
assert_eq!(stats.document_trie_path_nodes_copied, 17);
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!(
stats.document_resolver_lookups, stats.target_renders,
"each single-owner render resolves its root; retained context validation resolves no nodes"
@ -2693,6 +2733,60 @@ mod tests {
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]
fn retained_document_reuse_rejects_wrong_or_ambiguous_revision() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();

View 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);
}
}
}

View 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());
}