use std::collections::{BTreeMap, HashSet};
use std::sync::Arc;
use crate::pane::{
PANE_TREE_SCHEMA_VERSION, PaneConstraints, PaneId, PaneLeaf, PaneModelError, PaneNodeKind,
PaneNodeRecord, PaneOperation, PaneOperationKind, PanePlacement, PaneSplit, PaneSplitRatio,
PaneTree, PaneTreeSnapshot, SplitAxis,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PersistentNode {
Leaf {
id: PaneId,
constraints: PaneConstraints,
node_extensions: BTreeMap<String, String>,
leaf: PaneLeaf,
},
Split {
id: PaneId,
constraints: PaneConstraints,
node_extensions: BTreeMap<String, String>,
axis: SplitAxis,
ratio: PaneSplitRatio,
first: Arc<PersistentNode>,
second: Arc<PersistentNode>,
},
}
impl PersistentNode {
#[must_use]
pub const fn id(&self) -> PaneId {
match self {
Self::Leaf { id, .. } | Self::Split { id, .. } => *id,
}
}
#[must_use]
pub const fn is_leaf(&self) -> bool {
matches!(self, Self::Leaf { .. })
}
#[must_use]
pub fn node_count(&self) -> usize {
match self {
Self::Leaf { .. } => 1,
Self::Split { first, second, .. } => 1 + first.node_count() + second.node_count(),
}
}
#[must_use]
pub fn depth(&self) -> usize {
match self {
Self::Leaf { .. } => 1,
Self::Split { first, second, .. } => 1 + first.depth().max(second.depth()),
}
}
}
#[derive(Debug, Clone)]
pub struct VersionedPaneTree {
schema_version: u16,
root: Arc<PersistentNode>,
next_id: PaneId,
extensions: BTreeMap<String, String>,
}
impl VersionedPaneTree {
#[must_use]
pub fn singleton(surface_key: impl Into<String>) -> Self {
let root = PaneId::MIN;
let node = Arc::new(PersistentNode::Leaf {
id: root,
constraints: PaneConstraints::default(),
node_extensions: BTreeMap::new(),
leaf: PaneLeaf::new(surface_key),
});
Self {
schema_version: PANE_TREE_SCHEMA_VERSION,
root: node,
next_id: root.checked_next().unwrap_or(root),
extensions: BTreeMap::new(),
}
}
#[must_use]
pub fn from_pane_tree(tree: &PaneTree) -> Self {
Self::from_snapshot(&tree.to_snapshot())
}
#[must_use]
pub fn from_snapshot(snapshot: &PaneTreeSnapshot) -> Self {
let mut records: BTreeMap<PaneId, &PaneNodeRecord> = BTreeMap::new();
for record in &snapshot.nodes {
let _ = records.insert(record.id, record);
}
let root = build_persistent(&records, snapshot.root);
Self {
schema_version: snapshot.schema_version,
root,
next_id: snapshot.next_id,
extensions: snapshot.extensions.clone(),
}
}
#[must_use]
pub fn root(&self) -> &Arc<PersistentNode> {
&self.root
}
#[must_use]
pub fn root_id(&self) -> PaneId {
self.root.id()
}
#[must_use]
pub const fn next_id(&self) -> PaneId {
self.next_id
}
#[must_use]
pub const fn schema_version(&self) -> u16 {
self.schema_version
}
#[must_use]
pub fn node_count(&self) -> usize {
self.root.node_count()
}
#[must_use]
pub fn depth(&self) -> usize {
self.root.depth()
}
#[must_use]
pub fn to_snapshot(&self) -> PaneTreeSnapshot {
let mut nodes = Vec::with_capacity(self.node_count());
flatten_persistent(&self.root, None, &mut nodes);
let mut snapshot = PaneTreeSnapshot {
schema_version: self.schema_version,
root: self.root.id(),
next_id: self.next_id,
nodes,
extensions: self.extensions.clone(),
};
snapshot.canonicalize();
snapshot
}
pub fn to_pane_tree(&self) -> Result<PaneTree, PaneModelError> {
PaneTree::from_snapshot(self.to_snapshot())
}
pub fn state_hash(&self) -> Result<u64, PaneModelError> {
Ok(self.to_pane_tree()?.state_hash())
}
pub fn apply_operation(&self, operation: &PaneOperation) -> Result<Self, PersistentApplyError> {
match operation {
PaneOperation::SetSplitRatio { split, ratio } => {
self.apply_set_split_ratio(*split, *ratio)
}
PaneOperation::SplitLeaf {
target,
axis,
ratio,
placement,
new_leaf,
} => self.apply_split_leaf(*target, *axis, *ratio, *placement, new_leaf),
PaneOperation::CloseNode { target } => self.apply_close_node(*target),
PaneOperation::SwapNodes { first, second } => self.apply_swap_nodes(*first, *second),
PaneOperation::MoveSubtree { .. } | PaneOperation::NormalizeRatios => {
self.apply_via_rebuild(operation)
}
}
}
#[must_use]
pub const fn operation_strategy(kind: PaneOperationKind) -> PersistentApplyStrategy {
match kind {
PaneOperationKind::SetSplitRatio
| PaneOperationKind::SplitLeaf
| PaneOperationKind::CloseNode
| PaneOperationKind::SwapNodes => PersistentApplyStrategy::PathCopy,
PaneOperationKind::MoveSubtree | PaneOperationKind::NormalizeRatios => {
PersistentApplyStrategy::Rebuild
}
}
}
fn with_root(&self, root: Arc<PersistentNode>, next_id: PaneId) -> Self {
Self {
schema_version: self.schema_version,
root,
next_id,
extensions: self.extensions.clone(),
}
}
fn apply_set_split_ratio(
&self,
split: PaneId,
ratio: PaneSplitRatio,
) -> Result<Self, PersistentApplyError> {
match rebuild_set_ratio(&self.root, split, ratio)? {
Some(new_root) => Ok(self.with_root(new_root, self.next_id)),
None => Err(PersistentApplyError::MissingNode { node_id: split }),
}
}
fn apply_split_leaf(
&self,
target: PaneId,
axis: SplitAxis,
ratio: PaneSplitRatio,
placement: PanePlacement,
new_leaf: &PaneLeaf,
) -> Result<Self, PersistentApplyError> {
let mut next_id = self.next_id;
match rebuild_split_leaf(
&self.root,
target,
axis,
ratio,
placement,
new_leaf,
&mut next_id,
)? {
Some(new_root) => Ok(self.with_root(new_root, next_id)),
None => Err(PersistentApplyError::MissingNode { node_id: target }),
}
}
fn apply_close_node(&self, target: PaneId) -> Result<Self, PersistentApplyError> {
if target == self.root.id() {
return Err(PersistentApplyError::CannotCloseRoot { node_id: target });
}
match rebuild_close_node(&self.root, target) {
Some(new_root) => Ok(self.with_root(new_root, self.next_id)),
None => Err(PersistentApplyError::MissingNode { node_id: target }),
}
}
fn apply_swap_nodes(
&self,
first: PaneId,
second: PaneId,
) -> Result<Self, PersistentApplyError> {
if first == second {
return Ok(self.clone());
}
let Some(first_subtree) = find_subtree(&self.root, first) else {
return Err(PersistentApplyError::MissingNode { node_id: first });
};
let Some(second_subtree) = find_subtree(&self.root, second) else {
return Err(PersistentApplyError::MissingNode { node_id: second });
};
if subtree_contains(&first_subtree, second) {
return Err(PersistentApplyError::AncestorConflict {
ancestor: first,
descendant: second,
});
}
if subtree_contains(&second_subtree, first) {
return Err(PersistentApplyError::AncestorConflict {
ancestor: second,
descendant: first,
});
}
let new_root = replace_two(&self.root, first, &second_subtree, second, &first_subtree);
Ok(self.with_root(new_root, self.next_id))
}
fn apply_via_rebuild(&self, operation: &PaneOperation) -> Result<Self, PersistentApplyError> {
let mut tree = self
.to_pane_tree()
.map_err(PersistentApplyError::InvalidTree)?;
tree.apply_operation_conservative(0, operation.clone())
.map_err(|err| PersistentApplyError::Rebuild(format!("{:?}", err.reason)))?;
Ok(Self::from_pane_tree(&tree))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PersistentApplyStrategy {
PathCopy,
Rebuild,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PersistentApplyError {
MissingNode {
node_id: PaneId,
},
NotSplit {
node_id: PaneId,
},
NotLeaf {
node_id: PaneId,
},
CannotCloseRoot {
node_id: PaneId,
},
AncestorConflict {
ancestor: PaneId,
descendant: PaneId,
},
IdOverflow {
current: PaneId,
},
InvalidTree(PaneModelError),
Rebuild(String),
}
#[derive(Debug, Clone)]
pub struct PaneVersionStore {
versions: Vec<VersionedPaneTree>,
cursor: usize,
max_versions: usize,
pruned: usize,
}
impl PaneVersionStore {
#[must_use]
pub fn new(initial: VersionedPaneTree) -> Self {
Self {
versions: vec![initial],
cursor: 0,
max_versions: 0,
pruned: 0,
}
}
#[must_use]
pub fn with_max_versions(initial: VersionedPaneTree, max_versions: usize) -> Self {
let mut store = Self::new(initial);
store.max_versions = max_versions;
store
}
pub fn set_max_versions(&mut self, max_versions: usize) -> usize {
let pruned_before = self.pruned;
self.max_versions = max_versions;
self.enforce_retention();
self.pruned.saturating_sub(pruned_before)
}
pub fn apply(
&mut self,
operation: &PaneOperation,
) -> Result<&VersionedPaneTree, PersistentApplyError> {
let next = self.current().apply_operation(operation)?;
self.versions.truncate(self.cursor + 1);
self.versions.push(next);
self.cursor = self.versions.len() - 1;
self.enforce_retention();
Ok(self.current())
}
pub fn undo(&mut self) -> bool {
if self.cursor == 0 {
return false;
}
self.cursor -= 1;
true
}
pub fn redo(&mut self) -> bool {
if self.cursor + 1 >= self.versions.len() {
return false;
}
self.cursor += 1;
true
}
#[must_use]
pub fn current(&self) -> &VersionedPaneTree {
&self.versions[self.cursor]
}
#[must_use]
pub fn version_count(&self) -> usize {
self.versions.len()
}
#[must_use]
pub const fn cursor(&self) -> usize {
self.cursor
}
#[must_use]
pub const fn can_undo(&self) -> bool {
self.cursor > 0
}
#[must_use]
pub fn can_redo(&self) -> bool {
self.cursor + 1 < self.versions.len()
}
#[must_use]
pub const fn pruned(&self) -> usize {
self.pruned
}
#[must_use]
pub fn report(&self) -> PaneVersioningReport {
let mut distinct: HashSet<usize> = HashSet::new();
collect_distinct(
self.versions.iter().map(VersionedPaneTree::root),
&mut distinct,
);
let total_logical_nodes: usize = self
.versions
.iter()
.map(VersionedPaneTree::node_count)
.sum();
let distinct_nodes = distinct.len();
let shared_nodes = total_logical_nodes.saturating_sub(distinct_nodes);
let sharing_ratio = if total_logical_nodes == 0 {
0.0
} else {
shared_nodes as f64 / total_logical_nodes as f64
};
let current = self.current();
PaneVersioningReport {
version_count: self.versions.len(),
cursor: self.cursor,
distinct_nodes,
total_logical_nodes,
shared_nodes,
sharing_ratio,
current_node_count: current.node_count(),
current_depth: current.depth(),
pruned_versions: self.pruned,
}
}
#[must_use]
pub fn retention(&self) -> PaneVersionRetention {
let mut seen: HashSet<usize> = HashSet::new();
let mut distinct_node_count = 0usize;
let mut distinct_leaf_payload_bytes = 0usize;
let mut distinct_extension_payload_bytes = 0usize;
let mut stack: Vec<&Arc<PersistentNode>> =
self.versions.iter().map(VersionedPaneTree::root).collect();
while let Some(node) = stack.pop() {
if !seen.insert(Arc::as_ptr(node).addr()) {
continue;
}
distinct_node_count += 1;
match &**node {
PersistentNode::Leaf {
node_extensions,
leaf,
..
} => {
distinct_leaf_payload_bytes += leaf.surface_key.len();
distinct_extension_payload_bytes += string_map_payload_bytes(&leaf.extensions)
.saturating_add(string_map_payload_bytes(node_extensions));
}
PersistentNode::Split {
node_extensions,
first,
second,
..
} => {
distinct_extension_payload_bytes += string_map_payload_bytes(node_extensions);
stack.push(first);
stack.push(second);
}
}
}
let total_logical_node_count: usize = self
.versions
.iter()
.map(VersionedPaneTree::node_count)
.sum();
let arc_node_bytes = std::mem::size_of::<PersistentNode>().saturating_add(ARC_HEADER_BYTES);
let distinct_struct_bytes = distinct_node_count.saturating_mul(arc_node_bytes);
let version_metadata_bytes = self
.versions
.len()
.saturating_mul(std::mem::size_of::<VersionedPaneTree>());
let estimated_total_retained_bytes = std::mem::size_of::<Self>()
.saturating_add(distinct_struct_bytes)
.saturating_add(distinct_leaf_payload_bytes)
.saturating_add(distinct_extension_payload_bytes)
.saturating_add(version_metadata_bytes);
let shared_nodes = total_logical_node_count.saturating_sub(distinct_node_count);
let sharing_ratio = if total_logical_node_count == 0 {
0.0
} else {
shared_nodes as f64 / total_logical_node_count as f64
};
PaneVersionRetention {
version_count: self.versions.len(),
distinct_node_count,
total_logical_node_count,
sharing_ratio,
distinct_struct_bytes,
distinct_leaf_payload_bytes,
distinct_extension_payload_bytes,
version_metadata_bytes,
estimated_total_retained_bytes,
}
}
fn enforce_retention(&mut self) {
if self.max_versions == 0 || self.versions.len() <= self.max_versions {
return;
}
let drop_count = (self.versions.len() - self.max_versions).min(self.cursor);
if drop_count == 0 {
return;
}
let _ = self.versions.drain(0..drop_count);
self.pruned += drop_count;
self.cursor -= drop_count;
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct PaneVersioningReport {
pub version_count: usize,
pub cursor: usize,
pub distinct_nodes: usize,
pub total_logical_nodes: usize,
pub shared_nodes: usize,
pub sharing_ratio: f64,
pub current_node_count: usize,
pub current_depth: usize,
pub pruned_versions: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
pub struct PaneVersionRetention {
pub version_count: usize,
pub distinct_node_count: usize,
pub total_logical_node_count: usize,
pub sharing_ratio: f64,
pub distinct_struct_bytes: usize,
pub distinct_leaf_payload_bytes: usize,
pub distinct_extension_payload_bytes: usize,
pub version_metadata_bytes: usize,
pub estimated_total_retained_bytes: usize,
}
fn allocate_id(next_id: &mut PaneId) -> Result<PaneId, PersistentApplyError> {
let current = *next_id;
*next_id = current
.checked_next()
.map_err(|_| PersistentApplyError::IdOverflow { current })?;
Ok(current)
}
fn rebuild_split_with(
template: &Arc<PersistentNode>,
first: Arc<PersistentNode>,
second: Arc<PersistentNode>,
) -> Arc<PersistentNode> {
match &**template {
PersistentNode::Split {
id,
constraints,
node_extensions,
axis,
ratio,
..
} => Arc::new(PersistentNode::Split {
id: *id,
constraints: *constraints,
node_extensions: node_extensions.clone(),
axis: *axis,
ratio: *ratio,
first,
second,
}),
PersistentNode::Leaf { .. } => template.clone(),
}
}
fn rebuild_set_ratio(
node: &Arc<PersistentNode>,
target: PaneId,
ratio: PaneSplitRatio,
) -> Result<Option<Arc<PersistentNode>>, PersistentApplyError> {
match &**node {
PersistentNode::Leaf { id, .. } => {
if *id == target {
Err(PersistentApplyError::NotSplit { node_id: target })
} else {
Ok(None)
}
}
PersistentNode::Split {
id,
constraints,
node_extensions,
axis,
first,
second,
..
} => {
if *id == target {
return Ok(Some(Arc::new(PersistentNode::Split {
id: *id,
constraints: *constraints,
node_extensions: node_extensions.clone(),
axis: *axis,
ratio,
first: first.clone(),
second: second.clone(),
})));
}
if let Some(new_first) = rebuild_set_ratio(first, target, ratio)? {
return Ok(Some(rebuild_split_with(node, new_first, second.clone())));
}
if let Some(new_second) = rebuild_set_ratio(second, target, ratio)? {
return Ok(Some(rebuild_split_with(node, first.clone(), new_second)));
}
Ok(None)
}
}
}
#[allow(clippy::too_many_arguments)]
fn rebuild_split_leaf(
node: &Arc<PersistentNode>,
target: PaneId,
axis: SplitAxis,
ratio: PaneSplitRatio,
placement: PanePlacement,
new_leaf: &PaneLeaf,
next_id: &mut PaneId,
) -> Result<Option<Arc<PersistentNode>>, PersistentApplyError> {
match &**node {
PersistentNode::Leaf { id, .. } => {
if *id != target {
return Ok(None);
}
let split_id = allocate_id(next_id)?;
let new_leaf_id = allocate_id(next_id)?;
let existing = node.clone();
let incoming = Arc::new(PersistentNode::Leaf {
id: new_leaf_id,
constraints: PaneConstraints::default(),
node_extensions: BTreeMap::new(),
leaf: new_leaf.clone(),
});
let (first, second) = match placement {
PanePlacement::ExistingFirst => (existing, incoming),
PanePlacement::IncomingFirst => (incoming, existing),
};
Ok(Some(Arc::new(PersistentNode::Split {
id: split_id,
constraints: PaneConstraints::default(),
node_extensions: BTreeMap::new(),
axis,
ratio,
first,
second,
})))
}
PersistentNode::Split {
id, first, second, ..
} => {
if *id == target {
return Err(PersistentApplyError::NotLeaf { node_id: target });
}
if let Some(new_first) =
rebuild_split_leaf(first, target, axis, ratio, placement, new_leaf, next_id)?
{
return Ok(Some(rebuild_split_with(node, new_first, second.clone())));
}
if let Some(new_second) =
rebuild_split_leaf(second, target, axis, ratio, placement, new_leaf, next_id)?
{
return Ok(Some(rebuild_split_with(node, first.clone(), new_second)));
}
Ok(None)
}
}
}
fn rebuild_close_node(node: &Arc<PersistentNode>, target: PaneId) -> Option<Arc<PersistentNode>> {
let PersistentNode::Split { first, second, .. } = &**node else {
return None;
};
if first.id() == target {
return Some(second.clone());
}
if second.id() == target {
return Some(first.clone());
}
if let Some(new_first) = rebuild_close_node(first, target) {
return Some(rebuild_split_with(node, new_first, second.clone()));
}
if let Some(new_second) = rebuild_close_node(second, target) {
return Some(rebuild_split_with(node, first.clone(), new_second));
}
None
}
fn find_subtree(node: &Arc<PersistentNode>, target: PaneId) -> Option<Arc<PersistentNode>> {
if node.id() == target {
return Some(node.clone());
}
if let PersistentNode::Split { first, second, .. } = &**node {
if let Some(found) = find_subtree(first, target) {
return Some(found);
}
if let Some(found) = find_subtree(second, target) {
return Some(found);
}
}
None
}
fn subtree_contains(node: &Arc<PersistentNode>, target: PaneId) -> bool {
if node.id() == target {
return true;
}
match &**node {
PersistentNode::Leaf { .. } => false,
PersistentNode::Split { first, second, .. } => {
subtree_contains(first, target) || subtree_contains(second, target)
}
}
}
fn replace_two(
node: &Arc<PersistentNode>,
a: PaneId,
arc_a: &Arc<PersistentNode>,
b: PaneId,
arc_b: &Arc<PersistentNode>,
) -> Arc<PersistentNode> {
if node.id() == a {
return arc_a.clone();
}
if node.id() == b {
return arc_b.clone();
}
match &**node {
PersistentNode::Leaf { .. } => node.clone(),
PersistentNode::Split { first, second, .. } => {
let new_first = replace_two(first, a, arc_a, b, arc_b);
let new_second = replace_two(second, a, arc_a, b, arc_b);
if Arc::ptr_eq(&new_first, first) && Arc::ptr_eq(&new_second, second) {
node.clone()
} else {
rebuild_split_with(node, new_first, new_second)
}
}
}
}
fn build_persistent(
records: &BTreeMap<PaneId, &PaneNodeRecord>,
id: PaneId,
) -> Arc<PersistentNode> {
let record = records
.get(&id)
.copied()
.expect("snapshot references only existing ids");
match &record.kind {
PaneNodeKind::Leaf(leaf) => Arc::new(PersistentNode::Leaf {
id: record.id,
constraints: record.constraints,
node_extensions: record.extensions.clone(),
leaf: leaf.clone(),
}),
PaneNodeKind::Split(split) => Arc::new(PersistentNode::Split {
id: record.id,
constraints: record.constraints,
node_extensions: record.extensions.clone(),
axis: split.axis,
ratio: split.ratio,
first: build_persistent(records, split.first),
second: build_persistent(records, split.second),
}),
}
}
fn flatten_persistent(
node: &Arc<PersistentNode>,
parent: Option<PaneId>,
out: &mut Vec<PaneNodeRecord>,
) {
match &**node {
PersistentNode::Leaf {
id,
constraints,
node_extensions,
leaf,
} => out.push(PaneNodeRecord {
id: *id,
parent,
constraints: *constraints,
kind: PaneNodeKind::Leaf(leaf.clone()),
extensions: node_extensions.clone(),
}),
PersistentNode::Split {
id,
constraints,
node_extensions,
axis,
ratio,
first,
second,
} => {
out.push(PaneNodeRecord {
id: *id,
parent,
constraints: *constraints,
kind: PaneNodeKind::Split(PaneSplit {
axis: *axis,
ratio: *ratio,
first: first.id(),
second: second.id(),
}),
extensions: node_extensions.clone(),
});
flatten_persistent(first, Some(*id), out);
flatten_persistent(second, Some(*id), out);
}
}
}
const ARC_HEADER_BYTES: usize = 2 * std::mem::size_of::<usize>();
pub(crate) fn string_map_payload_bytes(map: &BTreeMap<String, String>) -> usize {
map.iter()
.map(|(key, value)| key.len().saturating_add(value.len()))
.sum()
}
fn collect_distinct<'a>(
roots: impl Iterator<Item = &'a Arc<PersistentNode>>,
seen: &mut HashSet<usize>,
) {
let mut stack: Vec<&Arc<PersistentNode>> = roots.collect();
while let Some(node) = stack.pop() {
let key = Arc::as_ptr(node).addr();
if !seen.insert(key) {
continue;
}
if let PersistentNode::Split { first, second, .. } = &**node {
stack.push(first);
stack.push(second);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pane::PaneTree;
fn ratio(n: u32, d: u32) -> PaneSplitRatio {
PaneSplitRatio::new(n, d).expect("valid ratio")
}
fn build_demo() -> VersionedPaneTree {
let v0 = VersionedPaneTree::singleton("root");
let op1 = PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Horizontal,
ratio: ratio(1, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("b"),
};
let v1 = v0.apply_operation(&op1).expect("split root");
let op2 = PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Vertical,
ratio: ratio(2, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("c"),
};
v1.apply_operation(&op2).expect("split leaf 1")
}
#[test]
fn singleton_round_trips_through_canonical_tree() {
let versioned = VersionedPaneTree::singleton("root");
let canonical = PaneTree::singleton("root");
assert_eq!(
versioned.state_hash().expect("hash"),
canonical.state_hash()
);
}
#[test]
fn split_leaf_matches_canonical_baseline() {
let versioned = build_demo();
let mut canonical = PaneTree::singleton("root");
canonical
.apply_operation_conservative(
1,
PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Horizontal,
ratio: ratio(1, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("b"),
},
)
.expect("split root");
canonical
.apply_operation_conservative(
2,
PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Vertical,
ratio: ratio(2, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("c"),
},
)
.expect("split leaf 1");
assert_eq!(
versioned.state_hash().expect("hash"),
canonical.state_hash()
);
assert_eq!(versioned.next_id(), canonical.next_id());
}
#[test]
fn set_split_ratio_preserves_off_path_sharing() {
let base = build_demo();
let before_second = match &**base.root() {
PersistentNode::Split { second, .. } => second.clone(),
PersistentNode::Leaf { .. } => unreachable!("root is a split"),
};
let next = base
.apply_operation(&PaneOperation::SetSplitRatio {
split: PaneId::new(4).unwrap(),
ratio: ratio(3, 1),
})
.expect("set ratio");
let after_second = match &**next.root() {
PersistentNode::Split { second, .. } => second.clone(),
PersistentNode::Leaf { .. } => unreachable!("root is a split"),
};
assert!(
Arc::ptr_eq(&before_second, &after_second),
"off-path subtree must be shared, not re-allocated"
);
assert!(!Arc::ptr_eq(base.root(), next.root()));
}
#[test]
fn set_split_ratio_on_leaf_is_rejected() {
let base = build_demo();
let err = base
.apply_operation(&PaneOperation::SetSplitRatio {
split: PaneId::MIN,
ratio: ratio(1, 1),
})
.expect_err("leaf is not a split");
assert_eq!(
err,
PersistentApplyError::NotSplit {
node_id: PaneId::MIN
}
);
}
#[test]
fn close_node_promotes_sibling_like_baseline() {
let base = build_demo();
let op = PaneOperation::CloseNode {
target: PaneId::new(3).unwrap(),
};
let next = base.apply_operation(&op).expect("close leaf 3");
let mut canonical = base.to_pane_tree().expect("flatten");
canonical
.apply_operation_conservative(99, op)
.expect("close leaf 3");
assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
}
#[test]
fn close_root_is_rejected() {
let base = build_demo();
let err = base
.apply_operation(&PaneOperation::CloseNode {
target: base.root_id(),
})
.expect_err("cannot close root");
assert!(matches!(err, PersistentApplyError::CannotCloseRoot { .. }));
}
#[test]
fn swap_nodes_matches_baseline_and_shares() {
let base = build_demo();
let op = PaneOperation::SwapNodes {
first: PaneId::new(3).unwrap(),
second: PaneId::new(5).unwrap(),
};
let next = base.apply_operation(&op).expect("swap leaves");
let mut canonical = base.to_pane_tree().expect("flatten");
canonical.apply_operation_conservative(7, op).expect("swap");
assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
}
#[test]
fn swap_with_self_is_noop() {
let base = build_demo();
let next = base
.apply_operation(&PaneOperation::SwapNodes {
first: PaneId::new(3).unwrap(),
second: PaneId::new(3).unwrap(),
})
.expect("self swap is a no-op");
assert_eq!(next.state_hash().expect("a"), base.state_hash().expect("b"));
}
#[test]
fn version_store_undo_redo_is_o1_and_correct() {
let mut store = PaneVersionStore::new(VersionedPaneTree::singleton("root"));
let h0 = store.current().state_hash().expect("h0");
store
.apply(&PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Horizontal,
ratio: ratio(1, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("b"),
})
.expect("split");
let h1 = store.current().state_hash().expect("h1");
assert_ne!(h0, h1);
assert!(store.undo());
assert_eq!(store.current().state_hash().expect("u"), h0);
assert!(store.redo());
assert_eq!(store.current().state_hash().expect("r"), h1);
assert!(!store.redo());
assert!(store.undo());
assert!(!store.undo());
}
#[test]
fn report_quantifies_sharing() {
let mut store = PaneVersionStore::new(build_demo());
for n in 1..=8u32 {
store
.apply(&PaneOperation::SetSplitRatio {
split: PaneId::new(4).unwrap(),
ratio: ratio(n, 1),
})
.expect("set ratio");
}
let report = store.report();
assert_eq!(report.version_count, 9);
assert!(
report.shared_nodes > 0,
"consecutive versions must share nodes"
);
assert!(report.distinct_nodes < report.total_logical_nodes);
assert!(report.sharing_ratio > 0.0 && report.sharing_ratio < 1.0);
}
fn ratio_storm_store() -> PaneVersionStore {
let mut store = PaneVersionStore::new(build_demo());
for n in 1..=8u32 {
store
.apply(&PaneOperation::SetSplitRatio {
split: PaneId::new(4).unwrap(),
ratio: ratio(n, 1),
})
.expect("set ratio");
}
store
}
#[test]
fn retention_model_is_deterministic() {
assert_eq!(
ratio_storm_store().retention(),
ratio_storm_store().retention()
);
}
#[test]
fn retention_total_is_faithful_sum_of_classes() {
let store = ratio_storm_store();
let r = store.retention();
let class_sum = std::mem::size_of::<PaneVersionStore>()
+ r.distinct_struct_bytes
+ r.distinct_leaf_payload_bytes
+ r.distinct_extension_payload_bytes
+ r.version_metadata_bytes;
assert_eq!(r.estimated_total_retained_bytes, class_sum);
assert_eq!(r.version_count, 9);
assert!(r.distinct_node_count < r.total_logical_node_count);
}
#[test]
fn retention_node_bytes_scale_with_distinct_not_logical_nodes() {
let store = ratio_storm_store();
let r = store.retention();
let per_node = std::mem::size_of::<PersistentNode>() + ARC_HEADER_BYTES;
assert_eq!(r.distinct_struct_bytes, r.distinct_node_count * per_node);
let naive_struct_bytes = r.total_logical_node_count * per_node;
assert!(
r.distinct_struct_bytes < naive_struct_bytes,
"sharing must cost fewer node-struct bytes than naive per-version snapshots"
);
assert!(r.sharing_ratio > 0.0 && r.sharing_ratio < 1.0);
}
#[test]
fn rebuild_fallback_normalize_ratios_matches_baseline() {
let base = build_demo();
let op = PaneOperation::NormalizeRatios;
let next = base.apply_operation(&op).expect("normalize");
let mut canonical = base.to_pane_tree().expect("flatten");
canonical
.apply_operation_conservative(11, op)
.expect("normalize baseline");
assert_eq!(next.state_hash().expect("hash"), canonical.state_hash());
assert_eq!(
VersionedPaneTree::operation_strategy(PaneOperationKind::NormalizeRatios),
PersistentApplyStrategy::Rebuild
);
}
}