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 {
502 let pruned_before = self.pruned;
503 self.max_versions = max_versions;
504 self.enforce_retention();
505 self.pruned.saturating_sub(pruned_before)
506 }
507
508 pub fn apply(
514 &mut self,
515 operation: &PaneOperation,
516 ) -> Result<&VersionedPaneTree, PersistentApplyError> {
517 let next = self.current().apply_operation(operation)?;
518 self.versions.truncate(self.cursor + 1);
520 self.versions.push(next);
521 self.cursor = self.versions.len() - 1;
522 self.enforce_retention();
523 Ok(self.current())
524 }
525
526 pub fn undo(&mut self) -> bool {
528 if self.cursor == 0 {
529 return false;
530 }
531 self.cursor -= 1;
532 true
533 }
534
535 pub fn redo(&mut self) -> bool {
537 if self.cursor + 1 >= self.versions.len() {
538 return false;
539 }
540 self.cursor += 1;
541 true
542 }
543
544 #[must_use]
546 pub fn current(&self) -> &VersionedPaneTree {
547 &self.versions[self.cursor]
548 }
549
550 #[must_use]
552 pub fn version_count(&self) -> usize {
553 self.versions.len()
554 }
555
556 #[must_use]
558 pub const fn cursor(&self) -> usize {
559 self.cursor
560 }
561
562 #[must_use]
564 pub const fn can_undo(&self) -> bool {
565 self.cursor > 0
566 }
567
568 #[must_use]
570 pub fn can_redo(&self) -> bool {
571 self.cursor + 1 < self.versions.len()
572 }
573
574 #[must_use]
576 pub const fn pruned(&self) -> usize {
577 self.pruned
578 }
579
580 #[must_use]
582 pub fn report(&self) -> PaneVersioningReport {
583 let mut distinct: HashSet<usize> = HashSet::new();
584 collect_distinct(
585 self.versions.iter().map(VersionedPaneTree::root),
586 &mut distinct,
587 );
588 let total_logical_nodes: usize = self
589 .versions
590 .iter()
591 .map(VersionedPaneTree::node_count)
592 .sum();
593 let distinct_nodes = distinct.len();
594 let shared_nodes = total_logical_nodes.saturating_sub(distinct_nodes);
595 let sharing_ratio = if total_logical_nodes == 0 {
596 0.0
597 } else {
598 shared_nodes as f64 / total_logical_nodes as f64
599 };
600 let current = self.current();
601 PaneVersioningReport {
602 version_count: self.versions.len(),
603 cursor: self.cursor,
604 distinct_nodes,
605 total_logical_nodes,
606 shared_nodes,
607 sharing_ratio,
608 current_node_count: current.node_count(),
609 current_depth: current.depth(),
610 pruned_versions: self.pruned,
611 }
612 }
613
614 #[must_use]
624 pub fn retention(&self) -> PaneVersionRetention {
625 let mut seen: HashSet<usize> = HashSet::new();
627 let mut distinct_node_count = 0usize;
628 let mut distinct_leaf_payload_bytes = 0usize;
629 let mut distinct_extension_payload_bytes = 0usize;
630 let mut stack: Vec<&Arc<PersistentNode>> =
631 self.versions.iter().map(VersionedPaneTree::root).collect();
632 while let Some(node) = stack.pop() {
633 if !seen.insert(Arc::as_ptr(node).addr()) {
634 continue;
635 }
636 distinct_node_count += 1;
637 match &**node {
638 PersistentNode::Leaf {
639 node_extensions,
640 leaf,
641 ..
642 } => {
643 distinct_leaf_payload_bytes += leaf.surface_key.len();
644 distinct_extension_payload_bytes += string_map_payload_bytes(&leaf.extensions)
645 .saturating_add(string_map_payload_bytes(node_extensions));
646 }
647 PersistentNode::Split {
648 node_extensions,
649 first,
650 second,
651 ..
652 } => {
653 distinct_extension_payload_bytes += string_map_payload_bytes(node_extensions);
654 stack.push(first);
655 stack.push(second);
656 }
657 }
658 }
659
660 let total_logical_node_count: usize = self
661 .versions
662 .iter()
663 .map(VersionedPaneTree::node_count)
664 .sum();
665 let arc_node_bytes = std::mem::size_of::<PersistentNode>().saturating_add(ARC_HEADER_BYTES);
666 let distinct_struct_bytes = distinct_node_count.saturating_mul(arc_node_bytes);
667 let version_metadata_bytes = self
668 .versions
669 .len()
670 .saturating_mul(std::mem::size_of::<VersionedPaneTree>());
671 let estimated_total_retained_bytes = std::mem::size_of::<Self>()
672 .saturating_add(distinct_struct_bytes)
673 .saturating_add(distinct_leaf_payload_bytes)
674 .saturating_add(distinct_extension_payload_bytes)
675 .saturating_add(version_metadata_bytes);
676 let shared_nodes = total_logical_node_count.saturating_sub(distinct_node_count);
677 let sharing_ratio = if total_logical_node_count == 0 {
678 0.0
679 } else {
680 shared_nodes as f64 / total_logical_node_count as f64
681 };
682
683 PaneVersionRetention {
684 version_count: self.versions.len(),
685 distinct_node_count,
686 total_logical_node_count,
687 sharing_ratio,
688 distinct_struct_bytes,
689 distinct_leaf_payload_bytes,
690 distinct_extension_payload_bytes,
691 version_metadata_bytes,
692 estimated_total_retained_bytes,
693 }
694 }
695
696 fn enforce_retention(&mut self) {
697 if self.max_versions == 0 || self.versions.len() <= self.max_versions {
698 return;
699 }
700 let drop_count = (self.versions.len() - self.max_versions).min(self.cursor);
705 if drop_count == 0 {
706 return;
707 }
708 let _ = self.versions.drain(0..drop_count);
709 self.pruned += drop_count;
710 self.cursor -= drop_count;
711 }
712}
713
714#[derive(Debug, Clone, PartialEq, serde::Serialize)]
716pub struct PaneVersioningReport {
717 pub version_count: usize,
719 pub cursor: usize,
721 pub distinct_nodes: usize,
723 pub total_logical_nodes: usize,
725 pub shared_nodes: usize,
727 pub sharing_ratio: f64,
729 pub current_node_count: usize,
731 pub current_depth: usize,
733 pub pruned_versions: usize,
735}
736
737#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
747pub struct PaneVersionRetention {
748 pub version_count: usize,
750 pub distinct_node_count: usize,
752 pub total_logical_node_count: usize,
754 pub sharing_ratio: f64,
756 pub distinct_struct_bytes: usize,
758 pub distinct_leaf_payload_bytes: usize,
760 pub distinct_extension_payload_bytes: usize,
762 pub version_metadata_bytes: usize,
764 pub estimated_total_retained_bytes: usize,
766}
767
768fn allocate_id(next_id: &mut PaneId) -> Result<PaneId, PersistentApplyError> {
779 let current = *next_id;
780 *next_id = current
781 .checked_next()
782 .map_err(|_| PersistentApplyError::IdOverflow { current })?;
783 Ok(current)
784}
785
786fn rebuild_split_with(
787 template: &Arc<PersistentNode>,
788 first: Arc<PersistentNode>,
789 second: Arc<PersistentNode>,
790) -> Arc<PersistentNode> {
791 match &**template {
792 PersistentNode::Split {
793 id,
794 constraints,
795 node_extensions,
796 axis,
797 ratio,
798 ..
799 } => Arc::new(PersistentNode::Split {
800 id: *id,
801 constraints: *constraints,
802 node_extensions: node_extensions.clone(),
803 axis: *axis,
804 ratio: *ratio,
805 first,
806 second,
807 }),
808 PersistentNode::Leaf { .. } => template.clone(),
810 }
811}
812
813fn rebuild_set_ratio(
814 node: &Arc<PersistentNode>,
815 target: PaneId,
816 ratio: PaneSplitRatio,
817) -> Result<Option<Arc<PersistentNode>>, PersistentApplyError> {
818 match &**node {
819 PersistentNode::Leaf { id, .. } => {
820 if *id == target {
821 Err(PersistentApplyError::NotSplit { node_id: target })
822 } else {
823 Ok(None)
824 }
825 }
826 PersistentNode::Split {
827 id,
828 constraints,
829 node_extensions,
830 axis,
831 first,
832 second,
833 ..
834 } => {
835 if *id == target {
836 return Ok(Some(Arc::new(PersistentNode::Split {
837 id: *id,
838 constraints: *constraints,
839 node_extensions: node_extensions.clone(),
840 axis: *axis,
841 ratio,
842 first: first.clone(),
843 second: second.clone(),
844 })));
845 }
846 if let Some(new_first) = rebuild_set_ratio(first, target, ratio)? {
847 return Ok(Some(rebuild_split_with(node, new_first, second.clone())));
848 }
849 if let Some(new_second) = rebuild_set_ratio(second, target, ratio)? {
850 return Ok(Some(rebuild_split_with(node, first.clone(), new_second)));
851 }
852 Ok(None)
853 }
854 }
855}
856
857#[allow(clippy::too_many_arguments)]
858fn rebuild_split_leaf(
859 node: &Arc<PersistentNode>,
860 target: PaneId,
861 axis: SplitAxis,
862 ratio: PaneSplitRatio,
863 placement: PanePlacement,
864 new_leaf: &PaneLeaf,
865 next_id: &mut PaneId,
866) -> Result<Option<Arc<PersistentNode>>, PersistentApplyError> {
867 match &**node {
868 PersistentNode::Leaf { id, .. } => {
869 if *id != target {
870 return Ok(None);
871 }
872 let split_id = allocate_id(next_id)?;
874 let new_leaf_id = allocate_id(next_id)?;
875 let existing = node.clone();
876 let incoming = Arc::new(PersistentNode::Leaf {
877 id: new_leaf_id,
878 constraints: PaneConstraints::default(),
879 node_extensions: BTreeMap::new(),
880 leaf: new_leaf.clone(),
881 });
882 let (first, second) = match placement {
883 PanePlacement::ExistingFirst => (existing, incoming),
884 PanePlacement::IncomingFirst => (incoming, existing),
885 };
886 Ok(Some(Arc::new(PersistentNode::Split {
887 id: split_id,
888 constraints: PaneConstraints::default(),
889 node_extensions: BTreeMap::new(),
890 axis,
891 ratio,
892 first,
893 second,
894 })))
895 }
896 PersistentNode::Split {
897 id, first, second, ..
898 } => {
899 if *id == target {
900 return Err(PersistentApplyError::NotLeaf { node_id: target });
901 }
902 if let Some(new_first) =
903 rebuild_split_leaf(first, target, axis, ratio, placement, new_leaf, next_id)?
904 {
905 return Ok(Some(rebuild_split_with(node, new_first, second.clone())));
906 }
907 if let Some(new_second) =
908 rebuild_split_leaf(second, target, axis, ratio, placement, new_leaf, next_id)?
909 {
910 return Ok(Some(rebuild_split_with(node, first.clone(), new_second)));
911 }
912 Ok(None)
913 }
914 }
915}
916
917fn rebuild_close_node(node: &Arc<PersistentNode>, target: PaneId) -> Option<Arc<PersistentNode>> {
920 let PersistentNode::Split { first, second, .. } = &**node else {
921 return None;
922 };
923 if first.id() == target {
924 return Some(second.clone());
925 }
926 if second.id() == target {
927 return Some(first.clone());
928 }
929 if let Some(new_first) = rebuild_close_node(first, target) {
930 return Some(rebuild_split_with(node, new_first, second.clone()));
931 }
932 if let Some(new_second) = rebuild_close_node(second, target) {
933 return Some(rebuild_split_with(node, first.clone(), new_second));
934 }
935 None
936}
937
938fn find_subtree(node: &Arc<PersistentNode>, target: PaneId) -> Option<Arc<PersistentNode>> {
940 if node.id() == target {
941 return Some(node.clone());
942 }
943 if let PersistentNode::Split { first, second, .. } = &**node {
944 if let Some(found) = find_subtree(first, target) {
945 return Some(found);
946 }
947 if let Some(found) = find_subtree(second, target) {
948 return Some(found);
949 }
950 }
951 None
952}
953
954fn subtree_contains(node: &Arc<PersistentNode>, target: PaneId) -> bool {
956 if node.id() == target {
957 return true;
958 }
959 match &**node {
960 PersistentNode::Leaf { .. } => false,
961 PersistentNode::Split { first, second, .. } => {
962 subtree_contains(first, target) || subtree_contains(second, target)
963 }
964 }
965}
966
967fn replace_two(
971 node: &Arc<PersistentNode>,
972 a: PaneId,
973 arc_a: &Arc<PersistentNode>,
974 b: PaneId,
975 arc_b: &Arc<PersistentNode>,
976) -> Arc<PersistentNode> {
977 if node.id() == a {
978 return arc_a.clone();
979 }
980 if node.id() == b {
981 return arc_b.clone();
982 }
983 match &**node {
984 PersistentNode::Leaf { .. } => node.clone(),
985 PersistentNode::Split { first, second, .. } => {
986 let new_first = replace_two(first, a, arc_a, b, arc_b);
987 let new_second = replace_two(second, a, arc_a, b, arc_b);
988 if Arc::ptr_eq(&new_first, first) && Arc::ptr_eq(&new_second, second) {
989 node.clone()
990 } else {
991 rebuild_split_with(node, new_first, new_second)
992 }
993 }
994 }
995}
996
997fn build_persistent(
998 records: &BTreeMap<PaneId, &PaneNodeRecord>,
999 id: PaneId,
1000) -> Arc<PersistentNode> {
1001 let record = records
1002 .get(&id)
1003 .copied()
1004 .expect("snapshot references only existing ids");
1005 match &record.kind {
1006 PaneNodeKind::Leaf(leaf) => Arc::new(PersistentNode::Leaf {
1007 id: record.id,
1008 constraints: record.constraints,
1009 node_extensions: record.extensions.clone(),
1010 leaf: leaf.clone(),
1011 }),
1012 PaneNodeKind::Split(split) => Arc::new(PersistentNode::Split {
1013 id: record.id,
1014 constraints: record.constraints,
1015 node_extensions: record.extensions.clone(),
1016 axis: split.axis,
1017 ratio: split.ratio,
1018 first: build_persistent(records, split.first),
1019 second: build_persistent(records, split.second),
1020 }),
1021 }
1022}
1023
1024fn flatten_persistent(
1025 node: &Arc<PersistentNode>,
1026 parent: Option<PaneId>,
1027 out: &mut Vec<PaneNodeRecord>,
1028) {
1029 match &**node {
1030 PersistentNode::Leaf {
1031 id,
1032 constraints,
1033 node_extensions,
1034 leaf,
1035 } => out.push(PaneNodeRecord {
1036 id: *id,
1037 parent,
1038 constraints: *constraints,
1039 kind: PaneNodeKind::Leaf(leaf.clone()),
1040 extensions: node_extensions.clone(),
1041 }),
1042 PersistentNode::Split {
1043 id,
1044 constraints,
1045 node_extensions,
1046 axis,
1047 ratio,
1048 first,
1049 second,
1050 } => {
1051 out.push(PaneNodeRecord {
1052 id: *id,
1053 parent,
1054 constraints: *constraints,
1055 kind: PaneNodeKind::Split(PaneSplit {
1056 axis: *axis,
1057 ratio: *ratio,
1058 first: first.id(),
1059 second: second.id(),
1060 }),
1061 extensions: node_extensions.clone(),
1062 });
1063 flatten_persistent(first, Some(*id), out);
1064 flatten_persistent(second, Some(*id), out);
1065 }
1066 }
1067}
1068
1069const ARC_HEADER_BYTES: usize = 2 * std::mem::size_of::<usize>();
1074
1075pub(crate) fn string_map_payload_bytes(map: &BTreeMap<String, String>) -> usize {
1081 map.iter()
1082 .map(|(key, value)| key.len().saturating_add(value.len()))
1083 .sum()
1084}
1085
1086fn collect_distinct<'a>(
1093 roots: impl Iterator<Item = &'a Arc<PersistentNode>>,
1094 seen: &mut HashSet<usize>,
1095) {
1096 let mut stack: Vec<&Arc<PersistentNode>> = roots.collect();
1097 while let Some(node) = stack.pop() {
1098 let key = Arc::as_ptr(node).addr();
1099 if !seen.insert(key) {
1100 continue;
1101 }
1102 if let PersistentNode::Split { first, second, .. } = &**node {
1103 stack.push(first);
1104 stack.push(second);
1105 }
1106 }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111 use super::*;
1112 use crate::pane::PaneTree;
1113
1114 fn ratio(n: u32, d: u32) -> PaneSplitRatio {
1115 PaneSplitRatio::new(n, d).expect("valid ratio")
1116 }
1117
1118 fn build_demo() -> VersionedPaneTree {
1121 let v0 = VersionedPaneTree::singleton("root");
1122 let op1 = PaneOperation::SplitLeaf {
1124 target: PaneId::MIN,
1125 axis: SplitAxis::Horizontal,
1126 ratio: ratio(1, 1),
1127 placement: PanePlacement::ExistingFirst,
1128 new_leaf: PaneLeaf::new("b"),
1129 };
1130 let v1 = v0.apply_operation(&op1).expect("split root");
1131 let op2 = PaneOperation::SplitLeaf {
1133 target: PaneId::MIN,
1134 axis: SplitAxis::Vertical,
1135 ratio: ratio(2, 1),
1136 placement: PanePlacement::ExistingFirst,
1137 new_leaf: PaneLeaf::new("c"),
1138 };
1139 v1.apply_operation(&op2).expect("split leaf 1")
1140 }
1141
1142 #[test]
1143 fn singleton_round_trips_through_canonical_tree() {
1144 let versioned = VersionedPaneTree::singleton("root");
1145 let canonical = PaneTree::singleton("root");
1146 assert_eq!(
1147 versioned.state_hash().expect("hash"),
1148 canonical.state_hash()
1149 );
1150 }
1151
1152 #[test]
1153 fn split_leaf_matches_canonical_baseline() {
1154 let versioned = build_demo();
1155 let mut canonical = PaneTree::singleton("root");
1156 canonical
1157 .apply_operation_conservative(
1158 1,
1159 PaneOperation::SplitLeaf {
1160 target: PaneId::MIN,
1161 axis: SplitAxis::Horizontal,
1162 ratio: ratio(1, 1),
1163 placement: PanePlacement::ExistingFirst,
1164 new_leaf: PaneLeaf::new("b"),
1165 },
1166 )
1167 .expect("split root");
1168 canonical
1169 .apply_operation_conservative(
1170 2,
1171 PaneOperation::SplitLeaf {
1172 target: PaneId::MIN,
1173 axis: SplitAxis::Vertical,
1174 ratio: ratio(2, 1),
1175 placement: PanePlacement::ExistingFirst,
1176 new_leaf: PaneLeaf::new("c"),
1177 },
1178 )
1179 .expect("split leaf 1");
1180 assert_eq!(
1181 versioned.state_hash().expect("hash"),
1182 canonical.state_hash()
1183 );
1184 assert_eq!(versioned.next_id(), canonical.next_id());
1185 }
1186
1187 #[test]
1188 fn set_split_ratio_preserves_off_path_sharing() {
1189 let base = build_demo();
1190 let before_second = match &**base.root() {
1193 PersistentNode::Split { second, .. } => second.clone(),
1194 PersistentNode::Leaf { .. } => unreachable!("root is a split"),
1195 };
1196 let next = base
1197 .apply_operation(&PaneOperation::SetSplitRatio {
1198 split: PaneId::new(4).unwrap(),
1199 ratio: ratio(3, 1),
1200 })
1201 .expect("set ratio");
1202 let after_second = match &**next.root() {
1203 PersistentNode::Split { second, .. } => second.clone(),
1204 PersistentNode::Leaf { .. } => unreachable!("root is a split"),
1205 };
1206 assert!(
1207 Arc::ptr_eq(&before_second, &after_second),
1208 "off-path subtree must be shared, not re-allocated"
1209 );
1210 assert!(!Arc::ptr_eq(base.root(), next.root()));
1212 }
1213
1214 #[test]
1215 fn set_split_ratio_on_leaf_is_rejected() {
1216 let base = build_demo();
1217 let err = base
1218 .apply_operation(&PaneOperation::SetSplitRatio {
1219 split: PaneId::MIN,
1220 ratio: ratio(1, 1),
1221 })
1222 .expect_err("leaf is not a split");
1223 assert_eq!(
1224 err,
1225 PersistentApplyError::NotSplit {
1226 node_id: PaneId::MIN
1227 }
1228 );
1229 }
1230
1231 #[test]
1232 fn close_node_promotes_sibling_like_baseline() {
1233 let base = build_demo();
1234 let op = PaneOperation::CloseNode {
1235 target: PaneId::new(3).unwrap(),
1236 };
1237 let next = base.apply_operation(&op).expect("close leaf 3");
1238
1239 let mut canonical = base.to_pane_tree().expect("flatten");
1240 canonical
1241 .apply_operation_conservative(99, op)
1242 .expect("close leaf 3");
1243 assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
1244 }
1245
1246 #[test]
1247 fn close_root_is_rejected() {
1248 let base = build_demo();
1249 let err = base
1250 .apply_operation(&PaneOperation::CloseNode {
1251 target: base.root_id(),
1252 })
1253 .expect_err("cannot close root");
1254 assert!(matches!(err, PersistentApplyError::CannotCloseRoot { .. }));
1255 }
1256
1257 #[test]
1258 fn swap_nodes_matches_baseline_and_shares() {
1259 let base = build_demo();
1260 let op = PaneOperation::SwapNodes {
1261 first: PaneId::new(3).unwrap(),
1262 second: PaneId::new(5).unwrap(),
1263 };
1264 let next = base.apply_operation(&op).expect("swap leaves");
1265
1266 let mut canonical = base.to_pane_tree().expect("flatten");
1267 canonical.apply_operation_conservative(7, op).expect("swap");
1268 assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
1269 }
1270
1271 #[test]
1272 fn swap_with_self_is_noop() {
1273 let base = build_demo();
1274 let next = base
1275 .apply_operation(&PaneOperation::SwapNodes {
1276 first: PaneId::new(3).unwrap(),
1277 second: PaneId::new(3).unwrap(),
1278 })
1279 .expect("self swap is a no-op");
1280 assert_eq!(next.state_hash().expect("a"), base.state_hash().expect("b"));
1281 }
1282
1283 #[test]
1284 fn version_store_undo_redo_is_o1_and_correct() {
1285 let mut store = PaneVersionStore::new(VersionedPaneTree::singleton("root"));
1286 let h0 = store.current().state_hash().expect("h0");
1287 store
1288 .apply(&PaneOperation::SplitLeaf {
1289 target: PaneId::MIN,
1290 axis: SplitAxis::Horizontal,
1291 ratio: ratio(1, 1),
1292 placement: PanePlacement::ExistingFirst,
1293 new_leaf: PaneLeaf::new("b"),
1294 })
1295 .expect("split");
1296 let h1 = store.current().state_hash().expect("h1");
1297 assert_ne!(h0, h1);
1298
1299 assert!(store.undo());
1300 assert_eq!(store.current().state_hash().expect("u"), h0);
1301 assert!(store.redo());
1302 assert_eq!(store.current().state_hash().expect("r"), h1);
1303 assert!(!store.redo());
1304 assert!(store.undo());
1305 assert!(!store.undo());
1306 }
1307
1308 #[test]
1309 fn report_quantifies_sharing() {
1310 let mut store = PaneVersionStore::new(build_demo());
1311 for n in 1..=8u32 {
1312 store
1313 .apply(&PaneOperation::SetSplitRatio {
1314 split: PaneId::new(4).unwrap(),
1315 ratio: ratio(n, 1),
1316 })
1317 .expect("set ratio");
1318 }
1319 let report = store.report();
1320 assert_eq!(report.version_count, 9);
1321 assert!(
1322 report.shared_nodes > 0,
1323 "consecutive versions must share nodes"
1324 );
1325 assert!(report.distinct_nodes < report.total_logical_nodes);
1326 assert!(report.sharing_ratio > 0.0 && report.sharing_ratio < 1.0);
1327 }
1328
1329 fn ratio_storm_store() -> PaneVersionStore {
1330 let mut store = PaneVersionStore::new(build_demo());
1331 for n in 1..=8u32 {
1332 store
1333 .apply(&PaneOperation::SetSplitRatio {
1334 split: PaneId::new(4).unwrap(),
1335 ratio: ratio(n, 1),
1336 })
1337 .expect("set ratio");
1338 }
1339 store
1340 }
1341
1342 #[test]
1343 fn retention_model_is_deterministic() {
1344 assert_eq!(
1348 ratio_storm_store().retention(),
1349 ratio_storm_store().retention()
1350 );
1351 }
1352
1353 #[test]
1354 fn retention_total_is_faithful_sum_of_classes() {
1355 let store = ratio_storm_store();
1356 let r = store.retention();
1357 let class_sum = std::mem::size_of::<PaneVersionStore>()
1358 + r.distinct_struct_bytes
1359 + r.distinct_leaf_payload_bytes
1360 + r.distinct_extension_payload_bytes
1361 + r.version_metadata_bytes;
1362 assert_eq!(r.estimated_total_retained_bytes, class_sum);
1363 assert_eq!(r.version_count, 9);
1364 assert!(r.distinct_node_count < r.total_logical_node_count);
1365 }
1366
1367 #[test]
1368 fn retention_node_bytes_scale_with_distinct_not_logical_nodes() {
1369 let store = ratio_storm_store();
1373 let r = store.retention();
1374 let per_node = std::mem::size_of::<PersistentNode>() + ARC_HEADER_BYTES;
1375 assert_eq!(r.distinct_struct_bytes, r.distinct_node_count * per_node);
1376 let naive_struct_bytes = r.total_logical_node_count * per_node;
1377 assert!(
1378 r.distinct_struct_bytes < naive_struct_bytes,
1379 "sharing must cost fewer node-struct bytes than naive per-version snapshots"
1380 );
1381 assert!(r.sharing_ratio > 0.0 && r.sharing_ratio < 1.0);
1382 }
1383
1384 #[test]
1385 fn rebuild_fallback_normalize_ratios_matches_baseline() {
1386 let base = build_demo();
1387 let op = PaneOperation::NormalizeRatios;
1388 let next = base.apply_operation(&op).expect("normalize");
1389 let mut canonical = base.to_pane_tree().expect("flatten");
1390 canonical
1391 .apply_operation_conservative(11, op)
1392 .expect("normalize baseline");
1393 assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
1394 assert_eq!(
1395 VersionedPaneTree::operation_strategy(PaneOperationKind::NormalizeRatios),
1396 PersistentApplyStrategy::Rebuild
1397 );
1398 }
1399}