1use std::collections::{BTreeMap, HashSet};
53use std::sync::Arc;
54
55use crate::pane::{
56 PANE_TREE_SCHEMA_VERSION, PaneConstraints, PaneId, PaneLeaf, PaneModelError, PaneNodeKind,
57 PaneNodeRecord, PaneOperation, PaneOperationKind, PanePlacement, PaneSplit, PaneSplitRatio,
58 PaneTree, PaneTreeSnapshot, SplitAxis,
59};
60
61#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum PersistentNode {
68 Leaf {
70 id: PaneId,
72 constraints: PaneConstraints,
74 node_extensions: BTreeMap<String, String>,
76 leaf: PaneLeaf,
78 },
79 Split {
81 id: PaneId,
83 constraints: PaneConstraints,
85 node_extensions: BTreeMap<String, String>,
87 axis: SplitAxis,
89 ratio: PaneSplitRatio,
91 first: Arc<PersistentNode>,
93 second: Arc<PersistentNode>,
95 },
96}
97
98impl PersistentNode {
99 #[must_use]
101 pub const fn id(&self) -> PaneId {
102 match self {
103 Self::Leaf { id, .. } | Self::Split { id, .. } => *id,
104 }
105 }
106
107 #[must_use]
109 pub const fn is_leaf(&self) -> bool {
110 matches!(self, Self::Leaf { .. })
111 }
112
113 #[must_use]
115 pub fn node_count(&self) -> usize {
116 match self {
117 Self::Leaf { .. } => 1,
118 Self::Split { first, second, .. } => 1 + first.node_count() + second.node_count(),
119 }
120 }
121
122 #[must_use]
124 pub fn depth(&self) -> usize {
125 match self {
126 Self::Leaf { .. } => 1,
127 Self::Split { first, second, .. } => 1 + first.depth().max(second.depth()),
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
137pub struct VersionedPaneTree {
138 schema_version: u16,
139 root: Arc<PersistentNode>,
140 next_id: PaneId,
141 extensions: BTreeMap<String, String>,
142}
143
144impl VersionedPaneTree {
145 #[must_use]
147 pub fn singleton(surface_key: impl Into<String>) -> Self {
148 let root = PaneId::MIN;
149 let node = Arc::new(PersistentNode::Leaf {
150 id: root,
151 constraints: PaneConstraints::default(),
152 node_extensions: BTreeMap::new(),
153 leaf: PaneLeaf::new(surface_key),
154 });
155 Self {
156 schema_version: PANE_TREE_SCHEMA_VERSION,
157 root: node,
158 next_id: root.checked_next().unwrap_or(root),
159 extensions: BTreeMap::new(),
160 }
161 }
162
163 #[must_use]
165 pub fn from_pane_tree(tree: &PaneTree) -> Self {
166 Self::from_snapshot(&tree.to_snapshot())
167 }
168
169 #[must_use]
171 pub fn from_snapshot(snapshot: &PaneTreeSnapshot) -> Self {
172 let mut records: BTreeMap<PaneId, &PaneNodeRecord> = BTreeMap::new();
173 for record in &snapshot.nodes {
174 let _ = records.insert(record.id, record);
175 }
176 let root = build_persistent(&records, snapshot.root);
177 Self {
178 schema_version: snapshot.schema_version,
179 root,
180 next_id: snapshot.next_id,
181 extensions: snapshot.extensions.clone(),
182 }
183 }
184
185 #[must_use]
187 pub fn root(&self) -> &Arc<PersistentNode> {
188 &self.root
189 }
190
191 #[must_use]
193 pub fn root_id(&self) -> PaneId {
194 self.root.id()
195 }
196
197 #[must_use]
199 pub const fn next_id(&self) -> PaneId {
200 self.next_id
201 }
202
203 #[must_use]
205 pub const fn schema_version(&self) -> u16 {
206 self.schema_version
207 }
208
209 #[must_use]
211 pub fn node_count(&self) -> usize {
212 self.root.node_count()
213 }
214
215 #[must_use]
217 pub fn depth(&self) -> usize {
218 self.root.depth()
219 }
220
221 #[must_use]
223 pub fn to_snapshot(&self) -> PaneTreeSnapshot {
224 let mut nodes = Vec::with_capacity(self.node_count());
225 flatten_persistent(&self.root, None, &mut nodes);
226 let mut snapshot = PaneTreeSnapshot {
227 schema_version: self.schema_version,
228 root: self.root.id(),
229 next_id: self.next_id,
230 nodes,
231 extensions: self.extensions.clone(),
232 };
233 snapshot.canonicalize();
234 snapshot
235 }
236
237 pub fn to_pane_tree(&self) -> Result<PaneTree, PaneModelError> {
248 PaneTree::from_snapshot(self.to_snapshot())
249 }
250
251 pub fn state_hash(&self) -> Result<u64, PaneModelError> {
258 Ok(self.to_pane_tree()?.state_hash())
259 }
260
261 pub fn apply_operation(&self, operation: &PaneOperation) -> Result<Self, PersistentApplyError> {
272 match operation {
273 PaneOperation::SetSplitRatio { split, ratio } => {
274 self.apply_set_split_ratio(*split, *ratio)
275 }
276 PaneOperation::SplitLeaf {
277 target,
278 axis,
279 ratio,
280 placement,
281 new_leaf,
282 } => self.apply_split_leaf(*target, *axis, *ratio, *placement, new_leaf),
283 PaneOperation::CloseNode { target } => self.apply_close_node(*target),
284 PaneOperation::SwapNodes { first, second } => self.apply_swap_nodes(*first, *second),
285 PaneOperation::MoveSubtree { .. } | PaneOperation::NormalizeRatios => {
286 self.apply_via_rebuild(operation)
287 }
288 }
289 }
290
291 #[must_use]
293 pub const fn operation_strategy(kind: PaneOperationKind) -> PersistentApplyStrategy {
294 match kind {
295 PaneOperationKind::SetSplitRatio
296 | PaneOperationKind::SplitLeaf
297 | PaneOperationKind::CloseNode
298 | PaneOperationKind::SwapNodes => PersistentApplyStrategy::PathCopy,
299 PaneOperationKind::MoveSubtree | PaneOperationKind::NormalizeRatios => {
300 PersistentApplyStrategy::Rebuild
301 }
302 }
303 }
304
305 fn with_root(&self, root: Arc<PersistentNode>, next_id: PaneId) -> Self {
306 Self {
307 schema_version: self.schema_version,
308 root,
309 next_id,
310 extensions: self.extensions.clone(),
311 }
312 }
313
314 fn apply_set_split_ratio(
315 &self,
316 split: PaneId,
317 ratio: PaneSplitRatio,
318 ) -> Result<Self, PersistentApplyError> {
319 match rebuild_set_ratio(&self.root, split, ratio)? {
322 Some(new_root) => Ok(self.with_root(new_root, self.next_id)),
323 None => Err(PersistentApplyError::MissingNode { node_id: split }),
324 }
325 }
326
327 fn apply_split_leaf(
328 &self,
329 target: PaneId,
330 axis: SplitAxis,
331 ratio: PaneSplitRatio,
332 placement: PanePlacement,
333 new_leaf: &PaneLeaf,
334 ) -> Result<Self, PersistentApplyError> {
335 let mut next_id = self.next_id;
336 match rebuild_split_leaf(
337 &self.root,
338 target,
339 axis,
340 ratio,
341 placement,
342 new_leaf,
343 &mut next_id,
344 )? {
345 Some(new_root) => Ok(self.with_root(new_root, next_id)),
346 None => Err(PersistentApplyError::MissingNode { node_id: target }),
347 }
348 }
349
350 fn apply_close_node(&self, target: PaneId) -> Result<Self, PersistentApplyError> {
351 if target == self.root.id() {
352 return Err(PersistentApplyError::CannotCloseRoot { node_id: target });
353 }
354 match rebuild_close_node(&self.root, target) {
355 Some(new_root) => Ok(self.with_root(new_root, self.next_id)),
356 None => Err(PersistentApplyError::MissingNode { node_id: target }),
357 }
358 }
359
360 fn apply_swap_nodes(
361 &self,
362 first: PaneId,
363 second: PaneId,
364 ) -> Result<Self, PersistentApplyError> {
365 if first == second {
366 return Ok(self.clone());
368 }
369 let Some(first_subtree) = find_subtree(&self.root, first) else {
370 return Err(PersistentApplyError::MissingNode { node_id: first });
371 };
372 let Some(second_subtree) = find_subtree(&self.root, second) else {
373 return Err(PersistentApplyError::MissingNode { node_id: second });
374 };
375 if subtree_contains(&first_subtree, second) {
376 return Err(PersistentApplyError::AncestorConflict {
377 ancestor: first,
378 descendant: second,
379 });
380 }
381 if subtree_contains(&second_subtree, first) {
382 return Err(PersistentApplyError::AncestorConflict {
383 ancestor: second,
384 descendant: first,
385 });
386 }
387 let new_root = replace_two(&self.root, first, &second_subtree, second, &first_subtree);
388 Ok(self.with_root(new_root, self.next_id))
389 }
390
391 fn apply_via_rebuild(&self, operation: &PaneOperation) -> Result<Self, PersistentApplyError> {
392 let mut tree = self
393 .to_pane_tree()
394 .map_err(PersistentApplyError::InvalidTree)?;
395 tree.apply_operation_conservative(0, operation.clone())
396 .map_err(|err| PersistentApplyError::Rebuild(format!("{:?}", err.reason)))?;
397 Ok(Self::from_pane_tree(&tree))
398 }
399}
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub enum PersistentApplyStrategy {
404 PathCopy,
406 Rebuild,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
416pub enum PersistentApplyError {
417 MissingNode {
419 node_id: PaneId,
421 },
422 NotSplit {
424 node_id: PaneId,
426 },
427 NotLeaf {
429 node_id: PaneId,
431 },
432 CannotCloseRoot {
434 node_id: PaneId,
436 },
437 AncestorConflict {
439 ancestor: PaneId,
441 descendant: PaneId,
443 },
444 IdOverflow {
446 current: PaneId,
448 },
449 InvalidTree(PaneModelError),
451 Rebuild(String),
453}
454
455#[derive(Debug, Clone)]
461pub struct PaneVersionStore {
462 versions: Vec<VersionedPaneTree>,
463 cursor: usize,
464 max_versions: usize,
465 pruned: usize,
466}
467
468impl PaneVersionStore {
469 #[must_use]
471 pub fn new(initial: VersionedPaneTree) -> Self {
472 Self {
473 versions: vec![initial],
474 cursor: 0,
475 max_versions: 0,
476 pruned: 0,
477 }
478 }
479
480 #[must_use]
486 pub fn with_max_versions(initial: VersionedPaneTree, max_versions: usize) -> Self {
487 let mut store = Self::new(initial);
488 store.max_versions = max_versions;
489 store
490 }
491
492 pub fn set_max_versions(&mut self, max_versions: usize) -> usize {
504 let pruned_before = self.pruned;
505 self.max_versions = max_versions;
506 self.enforce_retention();
507 self.pruned.saturating_sub(pruned_before)
508 }
509
510 pub fn apply(
516 &mut self,
517 operation: &PaneOperation,
518 ) -> Result<&VersionedPaneTree, PersistentApplyError> {
519 let next = self.current().apply_operation(operation)?;
520 self.versions.truncate(self.cursor + 1);
522 self.versions.push(next);
523 self.cursor = self.versions.len() - 1;
524 self.enforce_retention();
525 Ok(self.current())
526 }
527
528 pub fn undo(&mut self) -> bool {
530 if self.cursor == 0 {
531 return false;
532 }
533 self.cursor -= 1;
534 true
535 }
536
537 pub fn redo(&mut self) -> bool {
539 if self.cursor + 1 >= self.versions.len() {
540 return false;
541 }
542 self.cursor += 1;
543 true
544 }
545
546 #[must_use]
548 pub fn current(&self) -> &VersionedPaneTree {
549 &self.versions[self.cursor]
550 }
551
552 #[must_use]
554 pub fn version_count(&self) -> usize {
555 self.versions.len()
556 }
557
558 #[must_use]
560 pub const fn cursor(&self) -> usize {
561 self.cursor
562 }
563
564 #[must_use]
566 pub const fn can_undo(&self) -> bool {
567 self.cursor > 0
568 }
569
570 #[must_use]
572 pub fn can_redo(&self) -> bool {
573 self.cursor + 1 < self.versions.len()
574 }
575
576 #[must_use]
578 pub const fn pruned(&self) -> usize {
579 self.pruned
580 }
581
582 #[must_use]
584 pub fn report(&self) -> PaneVersioningReport {
585 let mut distinct: HashSet<usize> = HashSet::new();
586 collect_distinct(
587 self.versions.iter().map(VersionedPaneTree::root),
588 &mut distinct,
589 );
590 let total_logical_nodes: usize = self
591 .versions
592 .iter()
593 .map(VersionedPaneTree::node_count)
594 .sum();
595 let distinct_nodes = distinct.len();
596 let shared_nodes = total_logical_nodes.saturating_sub(distinct_nodes);
597 let sharing_ratio = if total_logical_nodes == 0 {
598 0.0
599 } else {
600 shared_nodes as f64 / total_logical_nodes as f64
601 };
602 let current = self.current();
603 PaneVersioningReport {
604 version_count: self.versions.len(),
605 cursor: self.cursor,
606 distinct_nodes,
607 total_logical_nodes,
608 shared_nodes,
609 sharing_ratio,
610 current_node_count: current.node_count(),
611 current_depth: current.depth(),
612 pruned_versions: self.pruned,
613 }
614 }
615
616 #[must_use]
626 pub fn retention(&self) -> PaneVersionRetention {
627 let mut seen: HashSet<usize> = HashSet::new();
629 let mut distinct_node_count = 0usize;
630 let mut distinct_leaf_payload_bytes = 0usize;
631 let mut distinct_extension_payload_bytes = 0usize;
632 let mut stack: Vec<&Arc<PersistentNode>> =
633 self.versions.iter().map(VersionedPaneTree::root).collect();
634 while let Some(node) = stack.pop() {
635 if !seen.insert(Arc::as_ptr(node).addr()) {
636 continue;
637 }
638 distinct_node_count += 1;
639 match &**node {
640 PersistentNode::Leaf {
641 node_extensions,
642 leaf,
643 ..
644 } => {
645 distinct_leaf_payload_bytes += leaf.surface_key.len();
646 distinct_extension_payload_bytes += string_map_payload_bytes(&leaf.extensions)
647 .saturating_add(string_map_payload_bytes(node_extensions));
648 }
649 PersistentNode::Split {
650 node_extensions,
651 first,
652 second,
653 ..
654 } => {
655 distinct_extension_payload_bytes += string_map_payload_bytes(node_extensions);
656 stack.push(first);
657 stack.push(second);
658 }
659 }
660 }
661
662 let total_logical_node_count: usize = self
663 .versions
664 .iter()
665 .map(VersionedPaneTree::node_count)
666 .sum();
667 let arc_node_bytes = std::mem::size_of::<PersistentNode>().saturating_add(ARC_HEADER_BYTES);
668 let distinct_struct_bytes = distinct_node_count.saturating_mul(arc_node_bytes);
669 let version_metadata_bytes = self
670 .versions
671 .len()
672 .saturating_mul(std::mem::size_of::<VersionedPaneTree>());
673 let estimated_total_retained_bytes = std::mem::size_of::<Self>()
674 .saturating_add(distinct_struct_bytes)
675 .saturating_add(distinct_leaf_payload_bytes)
676 .saturating_add(distinct_extension_payload_bytes)
677 .saturating_add(version_metadata_bytes);
678 let shared_nodes = total_logical_node_count.saturating_sub(distinct_node_count);
679 let sharing_ratio = if total_logical_node_count == 0 {
680 0.0
681 } else {
682 shared_nodes as f64 / total_logical_node_count as f64
683 };
684
685 PaneVersionRetention {
686 version_count: self.versions.len(),
687 distinct_node_count,
688 total_logical_node_count,
689 sharing_ratio,
690 distinct_struct_bytes,
691 distinct_leaf_payload_bytes,
692 distinct_extension_payload_bytes,
693 version_metadata_bytes,
694 estimated_total_retained_bytes,
695 }
696 }
697
698 fn enforce_retention(&mut self) {
699 if self.max_versions == 0 || self.versions.len() <= self.max_versions {
700 return;
701 }
702 let drop_count = (self.versions.len() - self.max_versions).min(self.cursor);
707 if drop_count == 0 {
708 return;
709 }
710 let _ = self.versions.drain(0..drop_count);
711 self.pruned += drop_count;
712 self.cursor -= drop_count;
713 }
714}
715
716#[derive(Debug, Clone, PartialEq, serde::Serialize)]
718pub struct PaneVersioningReport {
719 pub version_count: usize,
721 pub cursor: usize,
723 pub distinct_nodes: usize,
725 pub total_logical_nodes: usize,
727 pub shared_nodes: usize,
729 pub sharing_ratio: f64,
731 pub current_node_count: usize,
733 pub current_depth: usize,
735 pub pruned_versions: usize,
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
749pub struct PaneVersionRetention {
750 pub version_count: usize,
752 pub distinct_node_count: usize,
754 pub total_logical_node_count: usize,
756 pub sharing_ratio: f64,
758 pub distinct_struct_bytes: usize,
760 pub distinct_leaf_payload_bytes: usize,
762 pub distinct_extension_payload_bytes: usize,
764 pub version_metadata_bytes: usize,
766 pub estimated_total_retained_bytes: usize,
768}
769
770fn allocate_id(next_id: &mut PaneId) -> Result<PaneId, PersistentApplyError> {
781 let current = *next_id;
782 *next_id = current
783 .checked_next()
784 .map_err(|_| PersistentApplyError::IdOverflow { current })?;
785 Ok(current)
786}
787
788fn rebuild_split_with(
789 template: &Arc<PersistentNode>,
790 first: Arc<PersistentNode>,
791 second: Arc<PersistentNode>,
792) -> Arc<PersistentNode> {
793 match &**template {
794 PersistentNode::Split {
795 id,
796 constraints,
797 node_extensions,
798 axis,
799 ratio,
800 ..
801 } => Arc::new(PersistentNode::Split {
802 id: *id,
803 constraints: *constraints,
804 node_extensions: node_extensions.clone(),
805 axis: *axis,
806 ratio: *ratio,
807 first,
808 second,
809 }),
810 PersistentNode::Leaf { .. } => template.clone(),
812 }
813}
814
815fn rebuild_set_ratio(
816 node: &Arc<PersistentNode>,
817 target: PaneId,
818 ratio: PaneSplitRatio,
819) -> Result<Option<Arc<PersistentNode>>, PersistentApplyError> {
820 match &**node {
821 PersistentNode::Leaf { id, .. } => {
822 if *id == target {
823 Err(PersistentApplyError::NotSplit { node_id: target })
824 } else {
825 Ok(None)
826 }
827 }
828 PersistentNode::Split {
829 id,
830 constraints,
831 node_extensions,
832 axis,
833 first,
834 second,
835 ..
836 } => {
837 if *id == target {
838 return Ok(Some(Arc::new(PersistentNode::Split {
839 id: *id,
840 constraints: *constraints,
841 node_extensions: node_extensions.clone(),
842 axis: *axis,
843 ratio,
844 first: first.clone(),
845 second: second.clone(),
846 })));
847 }
848 if let Some(new_first) = rebuild_set_ratio(first, target, ratio)? {
849 return Ok(Some(rebuild_split_with(node, new_first, second.clone())));
850 }
851 if let Some(new_second) = rebuild_set_ratio(second, target, ratio)? {
852 return Ok(Some(rebuild_split_with(node, first.clone(), new_second)));
853 }
854 Ok(None)
855 }
856 }
857}
858
859#[allow(clippy::too_many_arguments)]
860fn rebuild_split_leaf(
861 node: &Arc<PersistentNode>,
862 target: PaneId,
863 axis: SplitAxis,
864 ratio: PaneSplitRatio,
865 placement: PanePlacement,
866 new_leaf: &PaneLeaf,
867 next_id: &mut PaneId,
868) -> Result<Option<Arc<PersistentNode>>, PersistentApplyError> {
869 match &**node {
870 PersistentNode::Leaf { id, .. } => {
871 if *id != target {
872 return Ok(None);
873 }
874 let split_id = allocate_id(next_id)?;
876 let new_leaf_id = allocate_id(next_id)?;
877 let existing = node.clone();
878 let incoming = Arc::new(PersistentNode::Leaf {
879 id: new_leaf_id,
880 constraints: PaneConstraints::default(),
881 node_extensions: BTreeMap::new(),
882 leaf: new_leaf.clone(),
883 });
884 let (first, second) = match placement {
885 PanePlacement::ExistingFirst => (existing, incoming),
886 PanePlacement::IncomingFirst => (incoming, existing),
887 };
888 Ok(Some(Arc::new(PersistentNode::Split {
889 id: split_id,
890 constraints: PaneConstraints::default(),
891 node_extensions: BTreeMap::new(),
892 axis,
893 ratio,
894 first,
895 second,
896 })))
897 }
898 PersistentNode::Split {
899 id, first, second, ..
900 } => {
901 if *id == target {
902 return Err(PersistentApplyError::NotLeaf { node_id: target });
903 }
904 if let Some(new_first) =
905 rebuild_split_leaf(first, target, axis, ratio, placement, new_leaf, next_id)?
906 {
907 return Ok(Some(rebuild_split_with(node, new_first, second.clone())));
908 }
909 if let Some(new_second) =
910 rebuild_split_leaf(second, target, axis, ratio, placement, new_leaf, next_id)?
911 {
912 return Ok(Some(rebuild_split_with(node, first.clone(), new_second)));
913 }
914 Ok(None)
915 }
916 }
917}
918
919fn rebuild_close_node(node: &Arc<PersistentNode>, target: PaneId) -> Option<Arc<PersistentNode>> {
922 let PersistentNode::Split { first, second, .. } = &**node else {
923 return None;
924 };
925 if first.id() == target {
926 return Some(second.clone());
927 }
928 if second.id() == target {
929 return Some(first.clone());
930 }
931 if let Some(new_first) = rebuild_close_node(first, target) {
932 return Some(rebuild_split_with(node, new_first, second.clone()));
933 }
934 if let Some(new_second) = rebuild_close_node(second, target) {
935 return Some(rebuild_split_with(node, first.clone(), new_second));
936 }
937 None
938}
939
940fn find_subtree(node: &Arc<PersistentNode>, target: PaneId) -> Option<Arc<PersistentNode>> {
942 if node.id() == target {
943 return Some(node.clone());
944 }
945 if let PersistentNode::Split { first, second, .. } = &**node {
946 if let Some(found) = find_subtree(first, target) {
947 return Some(found);
948 }
949 if let Some(found) = find_subtree(second, target) {
950 return Some(found);
951 }
952 }
953 None
954}
955
956fn subtree_contains(node: &Arc<PersistentNode>, target: PaneId) -> bool {
958 if node.id() == target {
959 return true;
960 }
961 match &**node {
962 PersistentNode::Leaf { .. } => false,
963 PersistentNode::Split { first, second, .. } => {
964 subtree_contains(first, target) || subtree_contains(second, target)
965 }
966 }
967}
968
969fn replace_two(
973 node: &Arc<PersistentNode>,
974 a: PaneId,
975 arc_a: &Arc<PersistentNode>,
976 b: PaneId,
977 arc_b: &Arc<PersistentNode>,
978) -> Arc<PersistentNode> {
979 if node.id() == a {
980 return arc_a.clone();
981 }
982 if node.id() == b {
983 return arc_b.clone();
984 }
985 match &**node {
986 PersistentNode::Leaf { .. } => node.clone(),
987 PersistentNode::Split { first, second, .. } => {
988 let new_first = replace_two(first, a, arc_a, b, arc_b);
989 let new_second = replace_two(second, a, arc_a, b, arc_b);
990 if Arc::ptr_eq(&new_first, first) && Arc::ptr_eq(&new_second, second) {
991 node.clone()
992 } else {
993 rebuild_split_with(node, new_first, new_second)
994 }
995 }
996 }
997}
998
999fn build_persistent(
1000 records: &BTreeMap<PaneId, &PaneNodeRecord>,
1001 id: PaneId,
1002) -> Arc<PersistentNode> {
1003 let record = records
1004 .get(&id)
1005 .copied()
1006 .expect("snapshot references only existing ids");
1007 match &record.kind {
1008 PaneNodeKind::Leaf(leaf) => Arc::new(PersistentNode::Leaf {
1009 id: record.id,
1010 constraints: record.constraints,
1011 node_extensions: record.extensions.clone(),
1012 leaf: leaf.clone(),
1013 }),
1014 PaneNodeKind::Split(split) => Arc::new(PersistentNode::Split {
1015 id: record.id,
1016 constraints: record.constraints,
1017 node_extensions: record.extensions.clone(),
1018 axis: split.axis,
1019 ratio: split.ratio,
1020 first: build_persistent(records, split.first),
1021 second: build_persistent(records, split.second),
1022 }),
1023 }
1024}
1025
1026fn flatten_persistent(
1027 node: &Arc<PersistentNode>,
1028 parent: Option<PaneId>,
1029 out: &mut Vec<PaneNodeRecord>,
1030) {
1031 match &**node {
1032 PersistentNode::Leaf {
1033 id,
1034 constraints,
1035 node_extensions,
1036 leaf,
1037 } => out.push(PaneNodeRecord {
1038 id: *id,
1039 parent,
1040 constraints: *constraints,
1041 kind: PaneNodeKind::Leaf(leaf.clone()),
1042 extensions: node_extensions.clone(),
1043 }),
1044 PersistentNode::Split {
1045 id,
1046 constraints,
1047 node_extensions,
1048 axis,
1049 ratio,
1050 first,
1051 second,
1052 } => {
1053 out.push(PaneNodeRecord {
1054 id: *id,
1055 parent,
1056 constraints: *constraints,
1057 kind: PaneNodeKind::Split(PaneSplit {
1058 axis: *axis,
1059 ratio: *ratio,
1060 first: first.id(),
1061 second: second.id(),
1062 }),
1063 extensions: node_extensions.clone(),
1064 });
1065 flatten_persistent(first, Some(*id), out);
1066 flatten_persistent(second, Some(*id), out);
1067 }
1068 }
1069}
1070
1071const ARC_HEADER_BYTES: usize = 2 * std::mem::size_of::<usize>();
1076
1077pub(crate) fn string_map_payload_bytes(map: &BTreeMap<String, String>) -> usize {
1083 map.iter()
1084 .map(|(key, value)| key.len().saturating_add(value.len()))
1085 .sum()
1086}
1087
1088fn collect_distinct<'a>(
1095 roots: impl Iterator<Item = &'a Arc<PersistentNode>>,
1096 seen: &mut HashSet<usize>,
1097) {
1098 let mut stack: Vec<&Arc<PersistentNode>> = roots.collect();
1099 while let Some(node) = stack.pop() {
1100 let key = Arc::as_ptr(node).addr();
1101 if !seen.insert(key) {
1102 continue;
1103 }
1104 if let PersistentNode::Split { first, second, .. } = &**node {
1105 stack.push(first);
1106 stack.push(second);
1107 }
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use crate::pane::PaneTree;
1115
1116 fn ratio(n: u32, d: u32) -> PaneSplitRatio {
1117 PaneSplitRatio::new(n, d).expect("valid ratio")
1118 }
1119
1120 fn build_demo() -> VersionedPaneTree {
1123 let v0 = VersionedPaneTree::singleton("root");
1124 let op1 = PaneOperation::SplitLeaf {
1126 target: PaneId::MIN,
1127 axis: SplitAxis::Horizontal,
1128 ratio: ratio(1, 1),
1129 placement: PanePlacement::ExistingFirst,
1130 new_leaf: PaneLeaf::new("b"),
1131 };
1132 let v1 = v0.apply_operation(&op1).expect("split root");
1133 let op2 = PaneOperation::SplitLeaf {
1135 target: PaneId::MIN,
1136 axis: SplitAxis::Vertical,
1137 ratio: ratio(2, 1),
1138 placement: PanePlacement::ExistingFirst,
1139 new_leaf: PaneLeaf::new("c"),
1140 };
1141 v1.apply_operation(&op2).expect("split leaf 1")
1142 }
1143
1144 #[test]
1145 fn singleton_round_trips_through_canonical_tree() {
1146 let versioned = VersionedPaneTree::singleton("root");
1147 let canonical = PaneTree::singleton("root");
1148 assert_eq!(
1149 versioned.state_hash().expect("hash"),
1150 canonical.state_hash()
1151 );
1152 }
1153
1154 #[test]
1155 fn split_leaf_matches_canonical_baseline() {
1156 let versioned = build_demo();
1157 let mut canonical = PaneTree::singleton("root");
1158 canonical
1159 .apply_operation_conservative(
1160 1,
1161 PaneOperation::SplitLeaf {
1162 target: PaneId::MIN,
1163 axis: SplitAxis::Horizontal,
1164 ratio: ratio(1, 1),
1165 placement: PanePlacement::ExistingFirst,
1166 new_leaf: PaneLeaf::new("b"),
1167 },
1168 )
1169 .expect("split root");
1170 canonical
1171 .apply_operation_conservative(
1172 2,
1173 PaneOperation::SplitLeaf {
1174 target: PaneId::MIN,
1175 axis: SplitAxis::Vertical,
1176 ratio: ratio(2, 1),
1177 placement: PanePlacement::ExistingFirst,
1178 new_leaf: PaneLeaf::new("c"),
1179 },
1180 )
1181 .expect("split leaf 1");
1182 assert_eq!(
1183 versioned.state_hash().expect("hash"),
1184 canonical.state_hash()
1185 );
1186 assert_eq!(versioned.next_id(), canonical.next_id());
1187 }
1188
1189 #[test]
1190 fn set_split_ratio_preserves_off_path_sharing() {
1191 let base = build_demo();
1192 let before_second = match &**base.root() {
1195 PersistentNode::Split { second, .. } => second.clone(),
1196 PersistentNode::Leaf { .. } => unreachable!("root is a split"),
1197 };
1198 let next = base
1199 .apply_operation(&PaneOperation::SetSplitRatio {
1200 split: PaneId::new(4).unwrap(),
1201 ratio: ratio(3, 1),
1202 })
1203 .expect("set ratio");
1204 let after_second = match &**next.root() {
1205 PersistentNode::Split { second, .. } => second.clone(),
1206 PersistentNode::Leaf { .. } => unreachable!("root is a split"),
1207 };
1208 assert!(
1209 Arc::ptr_eq(&before_second, &after_second),
1210 "off-path subtree must be shared, not re-allocated"
1211 );
1212 assert!(!Arc::ptr_eq(base.root(), next.root()));
1214 }
1215
1216 #[test]
1217 fn set_split_ratio_on_leaf_is_rejected() {
1218 let base = build_demo();
1219 let err = base
1220 .apply_operation(&PaneOperation::SetSplitRatio {
1221 split: PaneId::MIN,
1222 ratio: ratio(1, 1),
1223 })
1224 .expect_err("leaf is not a split");
1225 assert_eq!(
1226 err,
1227 PersistentApplyError::NotSplit {
1228 node_id: PaneId::MIN
1229 }
1230 );
1231 }
1232
1233 #[test]
1234 fn close_node_promotes_sibling_like_baseline() {
1235 let base = build_demo();
1236 let op = PaneOperation::CloseNode {
1237 target: PaneId::new(3).unwrap(),
1238 };
1239 let next = base.apply_operation(&op).expect("close leaf 3");
1240
1241 let mut canonical = base.to_pane_tree().expect("flatten");
1242 canonical
1243 .apply_operation_conservative(99, op)
1244 .expect("close leaf 3");
1245 assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
1246 }
1247
1248 #[test]
1249 fn close_root_is_rejected() {
1250 let base = build_demo();
1251 let err = base
1252 .apply_operation(&PaneOperation::CloseNode {
1253 target: base.root_id(),
1254 })
1255 .expect_err("cannot close root");
1256 assert!(matches!(err, PersistentApplyError::CannotCloseRoot { .. }));
1257 }
1258
1259 #[test]
1260 fn swap_nodes_matches_baseline_and_shares() {
1261 let base = build_demo();
1262 let op = PaneOperation::SwapNodes {
1263 first: PaneId::new(3).unwrap(),
1264 second: PaneId::new(5).unwrap(),
1265 };
1266 let next = base.apply_operation(&op).expect("swap leaves");
1267
1268 let mut canonical = base.to_pane_tree().expect("flatten");
1269 canonical.apply_operation_conservative(7, op).expect("swap");
1270 assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
1271 }
1272
1273 #[test]
1274 fn swap_with_self_is_noop() {
1275 let base = build_demo();
1276 let next = base
1277 .apply_operation(&PaneOperation::SwapNodes {
1278 first: PaneId::new(3).unwrap(),
1279 second: PaneId::new(3).unwrap(),
1280 })
1281 .expect("self swap is a no-op");
1282 assert_eq!(next.state_hash().expect("a"), base.state_hash().expect("b"));
1283 }
1284
1285 #[test]
1286 fn version_store_undo_redo_is_o1_and_correct() {
1287 let mut store = PaneVersionStore::new(VersionedPaneTree::singleton("root"));
1288 let h0 = store.current().state_hash().expect("h0");
1289 store
1290 .apply(&PaneOperation::SplitLeaf {
1291 target: PaneId::MIN,
1292 axis: SplitAxis::Horizontal,
1293 ratio: ratio(1, 1),
1294 placement: PanePlacement::ExistingFirst,
1295 new_leaf: PaneLeaf::new("b"),
1296 })
1297 .expect("split");
1298 let h1 = store.current().state_hash().expect("h1");
1299 assert_ne!(h0, h1);
1300
1301 assert!(store.undo());
1302 assert_eq!(store.current().state_hash().expect("u"), h0);
1303 assert!(store.redo());
1304 assert_eq!(store.current().state_hash().expect("r"), h1);
1305 assert!(!store.redo());
1306 assert!(store.undo());
1307 assert!(!store.undo());
1308 }
1309
1310 #[test]
1311 fn report_quantifies_sharing() {
1312 let mut store = PaneVersionStore::new(build_demo());
1313 for n in 1..=8u32 {
1314 store
1315 .apply(&PaneOperation::SetSplitRatio {
1316 split: PaneId::new(4).unwrap(),
1317 ratio: ratio(n, 1),
1318 })
1319 .expect("set ratio");
1320 }
1321 let report = store.report();
1322 assert_eq!(report.version_count, 9);
1323 assert!(
1324 report.shared_nodes > 0,
1325 "consecutive versions must share nodes"
1326 );
1327 assert!(report.distinct_nodes < report.total_logical_nodes);
1328 assert!(report.sharing_ratio > 0.0 && report.sharing_ratio < 1.0);
1329 }
1330
1331 fn ratio_storm_store() -> PaneVersionStore {
1332 let mut store = PaneVersionStore::new(build_demo());
1333 for n in 1..=8u32 {
1334 store
1335 .apply(&PaneOperation::SetSplitRatio {
1336 split: PaneId::new(4).unwrap(),
1337 ratio: ratio(n, 1),
1338 })
1339 .expect("set ratio");
1340 }
1341 store
1342 }
1343
1344 #[test]
1345 fn retention_model_is_deterministic() {
1346 assert_eq!(
1350 ratio_storm_store().retention(),
1351 ratio_storm_store().retention()
1352 );
1353 }
1354
1355 #[test]
1356 fn retention_total_is_faithful_sum_of_classes() {
1357 let store = ratio_storm_store();
1358 let r = store.retention();
1359 let class_sum = std::mem::size_of::<PaneVersionStore>()
1360 + r.distinct_struct_bytes
1361 + r.distinct_leaf_payload_bytes
1362 + r.distinct_extension_payload_bytes
1363 + r.version_metadata_bytes;
1364 assert_eq!(r.estimated_total_retained_bytes, class_sum);
1365 assert_eq!(r.version_count, 9);
1366 assert!(r.distinct_node_count < r.total_logical_node_count);
1367 }
1368
1369 #[test]
1370 fn retention_node_bytes_scale_with_distinct_not_logical_nodes() {
1371 let store = ratio_storm_store();
1375 let r = store.retention();
1376 let per_node = std::mem::size_of::<PersistentNode>() + ARC_HEADER_BYTES;
1377 assert_eq!(r.distinct_struct_bytes, r.distinct_node_count * per_node);
1378 let naive_struct_bytes = r.total_logical_node_count * per_node;
1379 assert!(
1380 r.distinct_struct_bytes < naive_struct_bytes,
1381 "sharing must cost fewer node-struct bytes than naive per-version snapshots"
1382 );
1383 assert!(r.sharing_ratio > 0.0 && r.sharing_ratio < 1.0);
1384 }
1385
1386 #[test]
1387 fn rebuild_fallback_normalize_ratios_matches_baseline() {
1388 let base = build_demo();
1389 let op = PaneOperation::NormalizeRatios;
1390 let next = base.apply_operation(&op).expect("normalize");
1391 let mut canonical = base.to_pane_tree().expect("flatten");
1392 canonical
1393 .apply_operation_conservative(11, op)
1394 .expect("normalize baseline");
1395 assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
1396 assert_eq!(
1397 VersionedPaneTree::operation_strategy(PaneOperationKind::NormalizeRatios),
1398 PersistentApplyStrategy::Rebuild
1399 );
1400 }
1401}