1use crate::pane::{
33 PaneInteractionTimeline, PaneNodeKind, PaneNodeRecord, PaneTree, PaneTreeSnapshot,
34};
35use crate::pane_persistent::{PaneVersionStore, string_map_payload_bytes};
36
37pub const PANE_MEMORY_TELEMETRY_SCHEMA_VERSION: u16 = 1;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
43pub enum PaneMemoryStrategy {
44 Baseline,
46 Checkpointed,
48 Persistent,
50}
51
52impl PaneMemoryStrategy {
53 #[must_use]
55 pub const fn as_str(self) -> &'static str {
56 match self {
57 Self::Baseline => "baseline",
58 Self::Checkpointed => "checkpointed",
59 Self::Persistent => "persistent",
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
66pub enum PaneMemoryDriver {
67 NodeStructs,
69 LeafPayload,
71 ExtensionPayload,
73 OperationLog,
75 StateHashes,
77 ContainerMetadata,
79}
80
81impl PaneMemoryDriver {
82 #[must_use]
84 pub const fn as_str(self) -> &'static str {
85 match self {
86 Self::NodeStructs => "node structs",
87 Self::LeafPayload => "leaf payload",
88 Self::ExtensionPayload => "extension payload",
89 Self::OperationLog => "operation log",
90 Self::StateHashes => "state hashes",
91 Self::ContainerMetadata => "container/metadata",
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)]
102pub struct PaneMemoryStrategyFootprint {
103 pub strategy: PaneMemoryStrategy,
105 pub retained_unit_count: usize,
109 pub retained_node_count: usize,
112 pub node_struct_bytes: usize,
114 pub leaf_payload_bytes: usize,
116 pub extension_payload_bytes: usize,
118 pub operation_payload_bytes: usize,
121 pub state_hash_overhead_bytes: usize,
123 pub container_and_metadata_bytes: usize,
125 pub total_retained_bytes: usize,
127 pub dominant_driver: PaneMemoryDriver,
129}
130
131impl PaneMemoryStrategyFootprint {
132 #[allow(clippy::too_many_arguments)]
133 fn assemble(
134 strategy: PaneMemoryStrategy,
135 retained_unit_count: usize,
136 retained_node_count: usize,
137 node_struct_bytes: usize,
138 leaf_payload_bytes: usize,
139 extension_payload_bytes: usize,
140 operation_payload_bytes: usize,
141 state_hash_overhead_bytes: usize,
142 total_retained_bytes: usize,
143 ) -> Self {
144 let attributed = node_struct_bytes
148 .saturating_add(leaf_payload_bytes)
149 .saturating_add(extension_payload_bytes)
150 .saturating_add(operation_payload_bytes)
151 .saturating_add(state_hash_overhead_bytes);
152 let container_and_metadata_bytes = total_retained_bytes.saturating_sub(attributed);
153 let dominant_driver = dominant_driver(
154 node_struct_bytes,
155 leaf_payload_bytes,
156 extension_payload_bytes,
157 operation_payload_bytes,
158 state_hash_overhead_bytes,
159 container_and_metadata_bytes,
160 );
161 Self {
162 strategy,
163 retained_unit_count,
164 retained_node_count,
165 node_struct_bytes,
166 leaf_payload_bytes,
167 extension_payload_bytes,
168 operation_payload_bytes,
169 state_hash_overhead_bytes,
170 container_and_metadata_bytes,
171 total_retained_bytes,
172 dominant_driver,
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, serde::Serialize)]
179pub struct PaneMemoryComparison {
180 pub schema_version: u16,
182 pub baseline: PaneMemoryStrategyFootprint,
184 pub checkpointed: PaneMemoryStrategyFootprint,
186 pub persistent: PaneMemoryStrategyFootprint,
188 pub persistent_over_checkpointed: f64,
190 pub checkpointed_over_baseline: f64,
192 pub most_compact_strategy: PaneMemoryStrategy,
194 pub summary: String,
196}
197
198#[must_use]
206pub fn pane_memory_comparison(
207 live: &PaneTree,
208 timeline: &PaneInteractionTimeline,
209 store: &PaneVersionStore,
210) -> PaneMemoryComparison {
211 let baseline = baseline_footprint(live);
212 let checkpointed = checkpointed_footprint(timeline);
213 let persistent = persistent_footprint(store);
214
215 let persistent_over_checkpointed = ratio(
216 persistent.total_retained_bytes,
217 checkpointed.total_retained_bytes,
218 );
219 let checkpointed_over_baseline = ratio(
220 checkpointed.total_retained_bytes,
221 baseline.total_retained_bytes,
222 );
223 let most_compact_strategy = most_compact(&baseline, &checkpointed, &persistent);
224
225 let summary = format!(
226 "baseline: {} tree, {}B (driver {}); checkpointed: {} snapshots / {} entries, {}B (driver {}); \
227 persistent: {} versions, {}B (driver {}). persistent/checkpointed={:.2}x, checkpointed/baseline={:.2}x; \
228 most compact: {}.",
229 baseline.retained_unit_count,
230 baseline.total_retained_bytes,
231 baseline.dominant_driver.as_str(),
232 checkpointed.retained_unit_count,
233 timeline.entries.len(),
234 checkpointed.total_retained_bytes,
235 checkpointed.dominant_driver.as_str(),
236 persistent.retained_unit_count,
237 persistent.total_retained_bytes,
238 persistent.dominant_driver.as_str(),
239 persistent_over_checkpointed,
240 checkpointed_over_baseline,
241 most_compact_strategy.as_str(),
242 );
243
244 PaneMemoryComparison {
245 schema_version: PANE_MEMORY_TELEMETRY_SCHEMA_VERSION,
246 baseline,
247 checkpointed,
248 persistent,
249 persistent_over_checkpointed,
250 checkpointed_over_baseline,
251 most_compact_strategy,
252 summary,
253 }
254}
255
256const STATE_HASH_BYTES: usize = std::mem::size_of::<u64>();
258
259fn baseline_footprint(live: &PaneTree) -> PaneMemoryStrategyFootprint {
260 let snapshot = live.to_snapshot();
261 let (leaf_payload_bytes, extension_payload_bytes) = snapshot_payload_bytes(&snapshot);
262 let node_struct_bytes = snapshot
263 .nodes
264 .len()
265 .saturating_mul(std::mem::size_of::<PaneNodeRecord>());
266 let container = std::mem::size_of::<PaneTreeSnapshot>();
267 let total = node_struct_bytes
268 .saturating_add(leaf_payload_bytes)
269 .saturating_add(extension_payload_bytes)
270 .saturating_add(container);
271 PaneMemoryStrategyFootprint::assemble(
272 PaneMemoryStrategy::Baseline,
273 1,
274 snapshot.nodes.len(),
275 node_struct_bytes,
276 leaf_payload_bytes,
277 extension_payload_bytes,
278 0,
279 0,
280 total,
281 )
282}
283
284fn checkpointed_footprint(timeline: &PaneInteractionTimeline) -> PaneMemoryStrategyFootprint {
285 let diag = timeline.retention_diagnostics();
286 let state_hash_overhead_bytes = diag.entry_count.saturating_mul(2 * STATE_HASH_BYTES);
289 let operation_payload_bytes = diag.retained_operation_payload_bytes.saturating_add(
290 diag.estimated_entry_struct_bytes
291 .saturating_sub(state_hash_overhead_bytes),
292 );
293 PaneMemoryStrategyFootprint::assemble(
294 PaneMemoryStrategy::Checkpointed,
295 diag.retained_snapshot_count,
296 diag.retained_snapshot_node_count,
297 diag.estimated_snapshot_struct_bytes,
298 diag.retained_leaf_payload_bytes,
299 diag.retained_extension_payload_bytes,
300 operation_payload_bytes,
301 state_hash_overhead_bytes,
302 diag.estimated_total_retained_bytes,
303 )
304}
305
306fn persistent_footprint(store: &PaneVersionStore) -> PaneMemoryStrategyFootprint {
307 let r = store.retention();
308 PaneMemoryStrategyFootprint::assemble(
309 PaneMemoryStrategy::Persistent,
310 r.version_count,
311 r.distinct_node_count,
312 r.distinct_struct_bytes,
313 r.distinct_leaf_payload_bytes,
314 r.distinct_extension_payload_bytes,
315 0,
316 0,
317 r.estimated_total_retained_bytes,
318 )
319}
320
321fn snapshot_payload_bytes(snapshot: &PaneTreeSnapshot) -> (usize, usize) {
323 let mut leaf_payload = 0usize;
324 let mut extension_payload = 0usize;
325 for record in &snapshot.nodes {
326 extension_payload =
327 extension_payload.saturating_add(string_map_payload_bytes(&record.extensions));
328 if let PaneNodeKind::Leaf(leaf) = &record.kind {
329 leaf_payload = leaf_payload.saturating_add(leaf.surface_key.len());
330 extension_payload =
331 extension_payload.saturating_add(string_map_payload_bytes(&leaf.extensions));
332 }
333 }
334 (leaf_payload, extension_payload)
335}
336
337fn dominant_driver(
340 node: usize,
341 leaf: usize,
342 ext: usize,
343 op: usize,
344 hash: usize,
345 container: usize,
346) -> PaneMemoryDriver {
347 let classes = [
348 (PaneMemoryDriver::NodeStructs, node),
349 (PaneMemoryDriver::LeafPayload, leaf),
350 (PaneMemoryDriver::ExtensionPayload, ext),
351 (PaneMemoryDriver::OperationLog, op),
352 (PaneMemoryDriver::StateHashes, hash),
353 (PaneMemoryDriver::ContainerMetadata, container),
354 ];
355 let mut best = classes[0];
356 for &candidate in &classes[1..] {
357 if candidate.1 > best.1 {
358 best = candidate;
359 }
360 }
361 best.0
362}
363
364fn ratio(numerator: usize, denominator: usize) -> f64 {
365 if denominator == 0 {
366 0.0
367 } else {
368 numerator as f64 / denominator as f64
369 }
370}
371
372fn most_compact(
373 baseline: &PaneMemoryStrategyFootprint,
374 checkpointed: &PaneMemoryStrategyFootprint,
375 persistent: &PaneMemoryStrategyFootprint,
376) -> PaneMemoryStrategy {
377 let mut best = baseline;
378 if checkpointed.total_retained_bytes < best.total_retained_bytes {
379 best = checkpointed;
380 }
381 if persistent.total_retained_bytes < best.total_retained_bytes {
382 best = persistent;
383 }
384 best.strategy
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::pane::{PaneId, PaneLeaf, PaneOperation, PanePlacement, PaneSplitRatio, SplitAxis};
391 use crate::pane_persistent::VersionedPaneTree;
392
393 fn ratio_of(n: u32, d: u32) -> PaneSplitRatio {
394 PaneSplitRatio::new(n, d).expect("valid ratio")
395 }
396
397 fn drive_workload() -> (PaneTree, PaneInteractionTimeline, PaneVersionStore) {
400 let mut ops: Vec<PaneOperation> = vec![
401 PaneOperation::SplitLeaf {
402 target: PaneId::MIN,
403 axis: SplitAxis::Horizontal,
404 ratio: ratio_of(1, 1),
405 placement: PanePlacement::ExistingFirst,
406 new_leaf: PaneLeaf::new("b"),
407 },
408 PaneOperation::SplitLeaf {
409 target: PaneId::MIN,
410 axis: SplitAxis::Vertical,
411 ratio: ratio_of(2, 1),
412 placement: PanePlacement::ExistingFirst,
413 new_leaf: PaneLeaf::new("c"),
414 },
415 ];
416 let split = PaneId::new(4).expect("valid id");
418 for n in 1..=6u32 {
419 ops.push(PaneOperation::SetSplitRatio {
420 split,
421 ratio: ratio_of(n, 1),
422 });
423 }
424
425 let mut tree = PaneTree::singleton("root");
426 let mut timeline = PaneInteractionTimeline::default();
427 let mut store = PaneVersionStore::new(VersionedPaneTree::singleton("root"));
428 for (i, op) in ops.iter().enumerate() {
429 let id = i as u64;
430 timeline
431 .apply_and_record(&mut tree, id, id, op.clone())
432 .expect("timeline apply");
433 store.apply(op).expect("store apply");
434 }
435 (tree, timeline, store)
436 }
437
438 fn assert_footprint_sums(f: &PaneMemoryStrategyFootprint) {
439 let class_sum = f.node_struct_bytes
440 + f.leaf_payload_bytes
441 + f.extension_payload_bytes
442 + f.operation_payload_bytes
443 + f.state_hash_overhead_bytes
444 + f.container_and_metadata_bytes;
445 assert_eq!(
446 class_sum, f.total_retained_bytes,
447 "{:?} class decomposition must sum to the authoritative total",
448 f.strategy
449 );
450 }
451
452 #[test]
453 fn comparison_is_deterministic() {
454 let (t1, tl1, s1) = drive_workload();
455 let (t2, tl2, s2) = drive_workload();
456 assert_eq!(
457 pane_memory_comparison(&t1, &tl1, &s1),
458 pane_memory_comparison(&t2, &tl2, &s2)
459 );
460 }
461
462 #[test]
463 fn every_footprint_decomposition_is_faithful() {
464 let (tree, timeline, store) = drive_workload();
465 let cmp = pane_memory_comparison(&tree, &timeline, &store);
466 assert_footprint_sums(&cmp.baseline);
467 assert_footprint_sums(&cmp.checkpointed);
468 assert_footprint_sums(&cmp.persistent);
469 assert_eq!(cmp.schema_version, PANE_MEMORY_TELEMETRY_SCHEMA_VERSION);
470 }
471
472 #[test]
473 fn baseline_is_the_floor_and_history_costs_more() {
474 let (tree, timeline, store) = drive_workload();
475 let cmp = pane_memory_comparison(&tree, &timeline, &store);
476 assert!(cmp.checkpointed.total_retained_bytes > cmp.baseline.total_retained_bytes);
478 assert!(cmp.persistent.total_retained_bytes > cmp.baseline.total_retained_bytes);
479 assert_eq!(cmp.most_compact_strategy, PaneMemoryStrategy::Baseline);
480 assert!(cmp.checkpointed_over_baseline > 1.0);
481 }
482
483 #[test]
484 fn only_checkpointed_pays_state_hash_and_op_log_overhead() {
485 let (tree, timeline, store) = drive_workload();
486 let cmp = pane_memory_comparison(&tree, &timeline, &store);
487 assert_eq!(cmp.persistent.state_hash_overhead_bytes, 0);
489 assert_eq!(cmp.persistent.operation_payload_bytes, 0);
490 assert_eq!(cmp.baseline.state_hash_overhead_bytes, 0);
491 assert_eq!(cmp.baseline.operation_payload_bytes, 0);
492 assert_eq!(
494 cmp.checkpointed.state_hash_overhead_bytes,
495 timeline.entries.len() * 2 * STATE_HASH_BYTES
496 );
497 assert!(cmp.checkpointed.state_hash_overhead_bytes > 0);
498 }
499
500 #[test]
501 fn checkpointed_defers_nodes_while_persistent_materializes_versions() {
502 let (tree, timeline, store) = drive_workload();
503 let cmp = pane_memory_comparison(&tree, &timeline, &store);
504 assert!(cmp.persistent.retained_node_count > cmp.checkpointed.retained_node_count);
511 assert!(cmp.checkpointed.operation_payload_bytes > 0);
512 assert_eq!(cmp.persistent.operation_payload_bytes, 0);
513 assert!(cmp.persistent_over_checkpointed > 0.0);
515 assert!(cmp.persistent_over_checkpointed.is_finite());
516 }
517
518 #[test]
519 fn dominant_driver_prefers_earlier_class_on_ties() {
520 assert_eq!(
522 dominant_driver(10, 10, 0, 0, 0, 0),
523 PaneMemoryDriver::NodeStructs
524 );
525 assert_eq!(
527 dominant_driver(1, 1, 1, 99, 1, 1),
528 PaneMemoryDriver::OperationLog
529 );
530 }
531}