use crate::pane::{
PaneInteractionTimeline, PaneNodeKind, PaneNodeRecord, PaneTree, PaneTreeSnapshot,
};
use crate::pane_persistent::{PaneVersionStore, string_map_payload_bytes};
pub const PANE_MEMORY_TELEMETRY_SCHEMA_VERSION: u16 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum PaneMemoryStrategy {
Baseline,
Checkpointed,
Persistent,
}
impl PaneMemoryStrategy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Baseline => "baseline",
Self::Checkpointed => "checkpointed",
Self::Persistent => "persistent",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum PaneMemoryDriver {
NodeStructs,
LeafPayload,
ExtensionPayload,
OperationLog,
StateHashes,
ContainerMetadata,
}
impl PaneMemoryDriver {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NodeStructs => "node structs",
Self::LeafPayload => "leaf payload",
Self::ExtensionPayload => "extension payload",
Self::OperationLog => "operation log",
Self::StateHashes => "state hashes",
Self::ContainerMetadata => "container/metadata",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
pub struct PaneMemoryStrategyFootprint {
pub strategy: PaneMemoryStrategy,
pub retained_unit_count: usize,
pub retained_node_count: usize,
pub node_struct_bytes: usize,
pub leaf_payload_bytes: usize,
pub extension_payload_bytes: usize,
pub operation_payload_bytes: usize,
pub state_hash_overhead_bytes: usize,
pub container_and_metadata_bytes: usize,
pub total_retained_bytes: usize,
pub dominant_driver: PaneMemoryDriver,
}
impl PaneMemoryStrategyFootprint {
#[allow(clippy::too_many_arguments)]
fn assemble(
strategy: PaneMemoryStrategy,
retained_unit_count: usize,
retained_node_count: usize,
node_struct_bytes: usize,
leaf_payload_bytes: usize,
extension_payload_bytes: usize,
operation_payload_bytes: usize,
state_hash_overhead_bytes: usize,
total_retained_bytes: usize,
) -> Self {
let attributed = node_struct_bytes
.saturating_add(leaf_payload_bytes)
.saturating_add(extension_payload_bytes)
.saturating_add(operation_payload_bytes)
.saturating_add(state_hash_overhead_bytes);
let container_and_metadata_bytes = total_retained_bytes.saturating_sub(attributed);
let dominant_driver = dominant_driver(
node_struct_bytes,
leaf_payload_bytes,
extension_payload_bytes,
operation_payload_bytes,
state_hash_overhead_bytes,
container_and_metadata_bytes,
);
Self {
strategy,
retained_unit_count,
retained_node_count,
node_struct_bytes,
leaf_payload_bytes,
extension_payload_bytes,
operation_payload_bytes,
state_hash_overhead_bytes,
container_and_metadata_bytes,
total_retained_bytes,
dominant_driver,
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct PaneMemoryComparison {
pub schema_version: u16,
pub baseline: PaneMemoryStrategyFootprint,
pub checkpointed: PaneMemoryStrategyFootprint,
pub persistent: PaneMemoryStrategyFootprint,
pub persistent_over_checkpointed: f64,
pub checkpointed_over_baseline: f64,
pub most_compact_strategy: PaneMemoryStrategy,
pub summary: String,
}
#[must_use]
pub fn pane_memory_comparison(
live: &PaneTree,
timeline: &PaneInteractionTimeline,
store: &PaneVersionStore,
) -> PaneMemoryComparison {
let baseline = baseline_footprint(live);
let checkpointed = checkpointed_footprint(timeline);
let persistent = persistent_footprint(store);
let persistent_over_checkpointed = ratio(
persistent.total_retained_bytes,
checkpointed.total_retained_bytes,
);
let checkpointed_over_baseline = ratio(
checkpointed.total_retained_bytes,
baseline.total_retained_bytes,
);
let most_compact_strategy = most_compact(&baseline, &checkpointed, &persistent);
let summary = format!(
"baseline: {} tree, {}B (driver {}); checkpointed: {} snapshots / {} entries, {}B (driver {}); \
persistent: {} versions, {}B (driver {}). persistent/checkpointed={:.2}x, checkpointed/baseline={:.2}x; \
most compact: {}.",
baseline.retained_unit_count,
baseline.total_retained_bytes,
baseline.dominant_driver.as_str(),
checkpointed.retained_unit_count,
timeline.entries.len(),
checkpointed.total_retained_bytes,
checkpointed.dominant_driver.as_str(),
persistent.retained_unit_count,
persistent.total_retained_bytes,
persistent.dominant_driver.as_str(),
persistent_over_checkpointed,
checkpointed_over_baseline,
most_compact_strategy.as_str(),
);
PaneMemoryComparison {
schema_version: PANE_MEMORY_TELEMETRY_SCHEMA_VERSION,
baseline,
checkpointed,
persistent,
persistent_over_checkpointed,
checkpointed_over_baseline,
most_compact_strategy,
summary,
}
}
const STATE_HASH_BYTES: usize = std::mem::size_of::<u64>();
fn baseline_footprint(live: &PaneTree) -> PaneMemoryStrategyFootprint {
let snapshot = live.to_snapshot();
let (leaf_payload_bytes, extension_payload_bytes) = snapshot_payload_bytes(&snapshot);
let node_struct_bytes = snapshot
.nodes
.len()
.saturating_mul(std::mem::size_of::<PaneNodeRecord>());
let container = std::mem::size_of::<PaneTreeSnapshot>();
let total = node_struct_bytes
.saturating_add(leaf_payload_bytes)
.saturating_add(extension_payload_bytes)
.saturating_add(container);
PaneMemoryStrategyFootprint::assemble(
PaneMemoryStrategy::Baseline,
1,
snapshot.nodes.len(),
node_struct_bytes,
leaf_payload_bytes,
extension_payload_bytes,
0,
0,
total,
)
}
fn checkpointed_footprint(timeline: &PaneInteractionTimeline) -> PaneMemoryStrategyFootprint {
let diag = timeline.retention_diagnostics();
let state_hash_overhead_bytes = diag.entry_count.saturating_mul(2 * STATE_HASH_BYTES);
let operation_payload_bytes = diag.retained_operation_payload_bytes.saturating_add(
diag.estimated_entry_struct_bytes
.saturating_sub(state_hash_overhead_bytes),
);
PaneMemoryStrategyFootprint::assemble(
PaneMemoryStrategy::Checkpointed,
diag.retained_snapshot_count,
diag.retained_snapshot_node_count,
diag.estimated_snapshot_struct_bytes,
diag.retained_leaf_payload_bytes,
diag.retained_extension_payload_bytes,
operation_payload_bytes,
state_hash_overhead_bytes,
diag.estimated_total_retained_bytes,
)
}
fn persistent_footprint(store: &PaneVersionStore) -> PaneMemoryStrategyFootprint {
let r = store.retention();
PaneMemoryStrategyFootprint::assemble(
PaneMemoryStrategy::Persistent,
r.version_count,
r.distinct_node_count,
r.distinct_struct_bytes,
r.distinct_leaf_payload_bytes,
r.distinct_extension_payload_bytes,
0,
0,
r.estimated_total_retained_bytes,
)
}
fn snapshot_payload_bytes(snapshot: &PaneTreeSnapshot) -> (usize, usize) {
let mut leaf_payload = 0usize;
let mut extension_payload = 0usize;
for record in &snapshot.nodes {
extension_payload =
extension_payload.saturating_add(string_map_payload_bytes(&record.extensions));
if let PaneNodeKind::Leaf(leaf) = &record.kind {
leaf_payload = leaf_payload.saturating_add(leaf.surface_key.len());
extension_payload =
extension_payload.saturating_add(string_map_payload_bytes(&leaf.extensions));
}
}
(leaf_payload, extension_payload)
}
fn dominant_driver(
node: usize,
leaf: usize,
ext: usize,
op: usize,
hash: usize,
container: usize,
) -> PaneMemoryDriver {
let classes = [
(PaneMemoryDriver::NodeStructs, node),
(PaneMemoryDriver::LeafPayload, leaf),
(PaneMemoryDriver::ExtensionPayload, ext),
(PaneMemoryDriver::OperationLog, op),
(PaneMemoryDriver::StateHashes, hash),
(PaneMemoryDriver::ContainerMetadata, container),
];
let mut best = classes[0];
for &candidate in &classes[1..] {
if candidate.1 > best.1 {
best = candidate;
}
}
best.0
}
fn ratio(numerator: usize, denominator: usize) -> f64 {
if denominator == 0 {
0.0
} else {
numerator as f64 / denominator as f64
}
}
fn most_compact(
baseline: &PaneMemoryStrategyFootprint,
checkpointed: &PaneMemoryStrategyFootprint,
persistent: &PaneMemoryStrategyFootprint,
) -> PaneMemoryStrategy {
let mut best = baseline;
if checkpointed.total_retained_bytes < best.total_retained_bytes {
best = checkpointed;
}
if persistent.total_retained_bytes < best.total_retained_bytes {
best = persistent;
}
best.strategy
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pane::{PaneId, PaneLeaf, PaneOperation, PanePlacement, PaneSplitRatio, SplitAxis};
use crate::pane_persistent::VersionedPaneTree;
fn ratio_of(n: u32, d: u32) -> PaneSplitRatio {
PaneSplitRatio::new(n, d).expect("valid ratio")
}
fn drive_workload() -> (PaneTree, PaneInteractionTimeline, PaneVersionStore) {
let mut ops: Vec<PaneOperation> = vec![
PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Horizontal,
ratio: ratio_of(1, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("b"),
},
PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Vertical,
ratio: ratio_of(2, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("c"),
},
];
let split = PaneId::new(4).expect("valid id");
for n in 1..=6u32 {
ops.push(PaneOperation::SetSplitRatio {
split,
ratio: ratio_of(n, 1),
});
}
let mut tree = PaneTree::singleton("root");
let mut timeline = PaneInteractionTimeline::default();
let mut store = PaneVersionStore::new(VersionedPaneTree::singleton("root"));
for (i, op) in ops.iter().enumerate() {
let id = i as u64;
timeline
.apply_and_record(&mut tree, id, id, op.clone())
.expect("timeline apply");
store.apply(op).expect("store apply");
}
(tree, timeline, store)
}
fn assert_footprint_sums(f: &PaneMemoryStrategyFootprint) {
let class_sum = f.node_struct_bytes
+ f.leaf_payload_bytes
+ f.extension_payload_bytes
+ f.operation_payload_bytes
+ f.state_hash_overhead_bytes
+ f.container_and_metadata_bytes;
assert_eq!(
class_sum, f.total_retained_bytes,
"{:?} class decomposition must sum to the authoritative total",
f.strategy
);
}
#[test]
fn comparison_is_deterministic() {
let (t1, tl1, s1) = drive_workload();
let (t2, tl2, s2) = drive_workload();
assert_eq!(
pane_memory_comparison(&t1, &tl1, &s1),
pane_memory_comparison(&t2, &tl2, &s2)
);
}
#[test]
fn every_footprint_decomposition_is_faithful() {
let (tree, timeline, store) = drive_workload();
let cmp = pane_memory_comparison(&tree, &timeline, &store);
assert_footprint_sums(&cmp.baseline);
assert_footprint_sums(&cmp.checkpointed);
assert_footprint_sums(&cmp.persistent);
assert_eq!(cmp.schema_version, PANE_MEMORY_TELEMETRY_SCHEMA_VERSION);
}
#[test]
fn baseline_is_the_floor_and_history_costs_more() {
let (tree, timeline, store) = drive_workload();
let cmp = pane_memory_comparison(&tree, &timeline, &store);
assert!(cmp.checkpointed.total_retained_bytes > cmp.baseline.total_retained_bytes);
assert!(cmp.persistent.total_retained_bytes > cmp.baseline.total_retained_bytes);
assert_eq!(cmp.most_compact_strategy, PaneMemoryStrategy::Baseline);
assert!(cmp.checkpointed_over_baseline > 1.0);
}
#[test]
fn only_checkpointed_pays_state_hash_and_op_log_overhead() {
let (tree, timeline, store) = drive_workload();
let cmp = pane_memory_comparison(&tree, &timeline, &store);
assert_eq!(cmp.persistent.state_hash_overhead_bytes, 0);
assert_eq!(cmp.persistent.operation_payload_bytes, 0);
assert_eq!(cmp.baseline.state_hash_overhead_bytes, 0);
assert_eq!(cmp.baseline.operation_payload_bytes, 0);
assert_eq!(
cmp.checkpointed.state_hash_overhead_bytes,
timeline.entries.len() * 2 * STATE_HASH_BYTES
);
assert!(cmp.checkpointed.state_hash_overhead_bytes > 0);
}
#[test]
fn checkpointed_defers_nodes_while_persistent_materializes_versions() {
let (tree, timeline, store) = drive_workload();
let cmp = pane_memory_comparison(&tree, &timeline, &store);
assert!(cmp.persistent.retained_node_count > cmp.checkpointed.retained_node_count);
assert!(cmp.checkpointed.operation_payload_bytes > 0);
assert_eq!(cmp.persistent.operation_payload_bytes, 0);
assert!(cmp.persistent_over_checkpointed > 0.0);
assert!(cmp.persistent_over_checkpointed.is_finite());
}
#[test]
fn dominant_driver_prefers_earlier_class_on_ties() {
assert_eq!(
dominant_driver(10, 10, 0, 0, 0, 0),
PaneMemoryDriver::NodeStructs
);
assert_eq!(
dominant_driver(1, 1, 1, 99, 1, 1),
PaneMemoryDriver::OperationLog
);
}
}