Cranpose Core
The core runtime engine for Cranpose. This crate implements the fundamental algorithms for managing the composition tree, state snapshots, and change detection.
When to Use
Use cranpose-core directly if you are:
- Building a custom tree management system unrelated to UI (e.g., a reactive scene graph).
- Implementing low-level state primitives.
- Developing a renderer for a strictly non-standard environment where the higher-level
cranpose-uimight be too opinionated.
Key Concepts
- Slot Table: The active slot-table runtime. It is a preorder group table backed by separate group, payload, and node storage. Active structure stays in the table; inactive retained branches are detached into explicit
DetachedSubtreeobjects instead of being encoded as gaps. - Composer: The primary interface for building and updating the slot table. It coordinates scopes, retention/disposal policy, remembered values, and node lifecycle on top of the semantic slot-storage API.
- Snapshot System: A multi-version concurrency control (MVCC) system for state. It allows
MutableStateto be read and written transactions, enabling atomic updates and ensuring UI consistency during concurrent operations. - Recomposition: The process of re-executing composable functions when their dependencies change. The runtime tracks dependencies at a fine-grained level (scopes) to minimize re-execution.
For the short invariant checklist the slot table must uphold, see docs/slot_table_invariants.md. For the design history behind the current architecture, see docs/cranpose_slot_table_v2_design.md (historical). Gap-table material is historical rationale only.
Example: Manual State Transaction
The following example shows how to perform atomic state updates using the snapshot system explicitly.
use ;
Internals: Snapshots and Slot Table System
This section documents the internals of the Snapshot and Slot Table systems, which form the foundation of the composition runtime.
Table of Contents
Overview
The cranpose runtime is built on two fundamental subsystems:
- Snapshot System: Provides Multi-Version Concurrency Control (MVCC) for state isolation, conflict detection, and optimistic merging
- Slot Table System: Manages the composition tree structure, enabling efficient recomposition and structural preservation
These systems work together but serve distinct purposes:
- Snapshots manage state values (what data is visible)
- Slot tables manage composition structure (where data is stored in the UI tree)
Snapshot System
Architecture Overview
The snapshot system implements a sophisticated MVCC mechanism that allows:
- Isolated views of mutable state
- Concurrent modifications without locks
- Optimistic conflict detection and merging
- Efficient garbage collection of obsolete records
Core Files
crates/cranpose-core/src/snapshot_v2/
├── mod.rs - Main types and coordination
├── runtime.rs - Global runtime state
├── mutable.rs - Mutable snapshot implementation
├── readonly.rs - Read-only snapshot implementation
├── nested.rs - Nested snapshot support
├── global.rs - Global snapshot
└── transparent.rs - Transparent observer snapshots
Supporting files:
├── state.rs - State objects and records
├── snapshot_id_set.rs - Optimized bit-set for IDs
├── snapshot_pinning.rs - Snapshot GC pinning
├── snapshot_weak_set.rs - Weak references to state
├── snapshot_double_index_heap.rs - Heap for pinning
└── snapshot_state_observer.rs - State observation
Data Structures
SnapshotIdSet
An optimized immutable bit-set for tracking snapshot IDs with O(1) access for recent snapshots:
Key Properties:
- Recent snapshots (128 most recent): O(1) bit operations
- Older snapshots: O(log N) binary search
- Immutable: All modifications create new instances (copy-on-write)
- Memory efficient: Two 64-bit integers cover 128 IDs
Operations:
get // O(1) for recent, O(log N) for old
set // O(1) for recent, O(N) for old (copy-on-write)
or // Combine two sets
and_not // Set difference
lowest // Find minimum ID
StateRecord
The fundamental unit of state versioning - a linked list node containing one version of a state value:
Record Chain Example:
SnapshotMutableState<i32>
head → [id=10, value=100, next] → [id=8, value=50, next] → [id=5, value=0, next] → None
↑ ↑ ↑
Latest Older Oldest
Special IDs:
INVALID_SNAPSHOT(SnapshotId::MAX): Marks records available for reuse- Valid IDs: Used to determine visibility to each snapshot
SnapshotMutableState<T>
The primary state object that applications interact with:
Usage:
let state = new;
let value = state.read; // Read with snapshot isolation
state.write; // Write creates new record
MutationPolicy: Defines how values are compared and merged:
StructuralEqualityPolicy: UsesPartialEqReferentialEqualityPolicy: UsesArcpointer equality- Custom policies can implement three-way merging
MutableSnapshot
A snapshot that can track writes and be applied to its parent:
SnapshotState (shared between snapshot types):
The modified map tracks all state objects written to in this snapshot:
- Key:
StateObjectId(unique object identifier) - Value:
(Arc<StateObject>, SnapshotId)- the object and writer snapshot ID
Snapshot Lifecycle
1. Creation
// In runtime.rs
Steps:
- Allocate new monotonically increasing ID
- Capture current
open_snapshotsas theinvalidset - Add new ID to
open_snapshots - Pin the snapshot ID to prevent GC
Why capture open_snapshots? Any snapshot currently open might write to state objects, so their writes should be invisible to this new snapshot until they're applied.
2. Reading State
Finding the readable record:
Key insight: Walk the chain, skip invalid/tombstone records, return the record with the highest valid ID ≤ snapshot_id.
3. Writing State
Creating/reusing writable records:
Record reuse is critical for performance - instead of creating infinite records, we reuse ones that are no longer visible to any snapshot.
4. Applying (Merging)
The most complex operation - merging a child snapshot's changes into its parent:
Three-way merge visualization:
Timeline:
t0: Create snapshot S1 (base_parent_id = G0)
previous = state.read(G0) = "A"
t1: Snapshot S1 writes "B"
applied = "B"
t2: Snapshot S2 writes "C" and applies
current = state.read(G1) = "C"
t3: Snapshot S1 tries to apply
previous = "A"
current = "C" (≠ previous, conflict detected!)
applied = "B"
Merge attempt: Can we merge "A" → "C" and "A" → "B"?
Merge strategies (from MutationPolicy):
- PromoteChild: No conflict (current == previous), use applied
- PromoteExisting: Merged value equals current, use current
- CommitMerged: Create new merged record
5. Disposal
Garbage Collection (Record Reuse System)
IMPORTANT: This is NOT traditional garbage collection. Rust's Arc already provides automatic memory management. This system is about record chain cleanup and reuse optimization.
Why Needed in Rust (Not Just Copy-Paste from Kotlin)
The Problem:
let state = new;
// Without record reuse:
for i in 0..1000
// Result: 1000 records in chain, even though only latest matters!
// Memory: ~64KB for records that will never be read
Why Rust's Arc Doesn't Help:
- Arc keeps records alive: Each record has
next: Cell<Option<Arc<StateRecord>>> - Chain references prevent collection: Head → Record1 → Record2 → Record3...
- Arc only frees when refcount = 0, but head always holds reference to entire chain
- Without cleanup: Infinite record chain growth = memory leak
What This System Actually Does:
- Identifies obsolete records: Records older than
lowest_pinned_snapshotcan't be read - Marks for reuse: Set
snapshot_id = INVALID_SNAPSHOTinstead of dropping - Reuses on next write:
writable_record()checks for INVALID records first - Prevents chain growth: Bounded memory regardless of write count
Real-World Impact
Without record reuse (hypothetical):
// UI counter that updates every frame (60 FPS)
let counter = new;
for frame in 0..3600
// Memory: 3600 records × 64 bytes = ~230 KB
// After 1 hour: ~13 MB just for one counter!
With record reuse:
// Same scenario, but records are reused
// Memory: ~3-10 records (bounded by concurrent snapshot count)
// Memory: ~200-640 bytes regardless of time
Actual Usage in Code
The cleanup runs automatically on every global write:
// state.rs:647
state.set(new_value);
↓
advance_global_snapshot(new_id); // state.rs:647
↓
check_and_overwrite_unused_records_locked(); // global.rs:190
↓
EXTRA_STATE_OBJECTS.remove_if(|state| {
state.overwrite_unused_records() // Cleanup happens here
});
Frequency: Every write to global snapshot (most common case).
The Algorithm Explained
Kotlin vs Rust: Why Both Need This
Kotlin (Original Compose):
- JVM GC collects unreferenced objects automatically
- Still needs record reuse because record chains hold strong references
- JVM GC won't collect records still referenced by chain
- Same problem: unbounded chain growth without manual cleanup
Rust (This Implementation):
Arcprovides automatic reference counting- Same problem as Kotlin: chain references prevent automatic cleanup
Arconly drops when refcount = 0, but chain maintains references- Not a copy-paste bug: Genuinely required for memory bounds
Key Insight: This isn't about memory safety (Rust guarantees that). It's about memory efficiency. Without this system, memory usage grows O(n) with write count instead of O(1).
Visual Example: Why Arc Alone Fails
// Initial state
state.head -> [id=1, value=0, next=None]
Arc::strong_count = 1
// After state.set(10)
state.head -> [id=2, value=10, next] -> [id=1, value=0, next=None]
Arc::strong_count = 1 Arc::strong_count = 1 ← Still alive!
// After state.set(20)
state.head -> [id=3, value=20, next] -> [id=2, value=10, next] -> [id=1, value=0, next=None]
↑ Can't drop: still referenced by id=3
// After 1000 writes: Chain of 1000 records, all kept alive by next pointers
// Arc can't help because references form a chain
// WITH record reuse:
state.head -> [id=1003, value=1000, next] -> [id=INVALID, reusable] -> [id=1, value=0, next=None]
↑ Latest ↑ Marked for reuse ↑ PREEXISTING (kept)
// Next write reuses INVALID record instead of allocating
Pinning System
Problem: Records can only be reused if no snapshot can read them.
Solution: Track the lowest snapshot ID that might read each record:
Reuse limit: Records with id < lowest_pinned_snapshot() are safe to reuse.
Record Cleanup
Key insight: Keep one historical record for potential rollback, mark older ones as INVALID for reuse.
Nested Snapshots
Snapshots can be nested to create isolation boundaries:
let outer = take_mutable_snapshot;
outer.apply?; // Merge into global
Nested tracking:
- Parent tracks
nested_countof active children - Parent cannot apply while children are alive
- Children's
base_parent_idpoints to parent's ID at creation time
Slot Table System
Architecture Overview
The active slot-table implementation separates active structure, payload storage, node identity, and retention into distinct layers, replacing the historical gap-buffer design.
It separates the runtime into distinct storage layers:
- Active structure lives in a preorder
Vec<GroupRecord>. - Remembered payloads live in a separate payload table.
- Node identities live in a separate node table.
- Inactive preserved branches live outside the active table as explicit
DetachedSubtreevalues.
That gives the runtime much simpler semantics:
- There is no semantic
Gapinside the active table. - Retention is explicit detach/restore, not preserved free space.
- Scopes are resolved through an index, not by scanning all groups.
- All structural mutation goes through the writer session state.
The invariants it must uphold are tracked in docs/slot_table_invariants.md; the design history behind this architecture is docs/cranpose_slot_table_v2_design.md (historical).
Core Files
crates/cranpose-core/src/
├── retention.rs - Detached-subtree retention bookkeeping
└── slot/
├── types.rs - Semantic handles, cursors, and operation result types
├── table.rs - SlotTable and write-session entry points
├── writer.rs - begin/finish/end/skip group traversal
├── table/ - SlotTable metadata, mutation, and value helpers
├── writer/ - Writer state-machine helper modules
├── groups.rs - GroupRecord helpers and child traversal
├── payload.rs - Payload storage and value-slot records
├── payload_anchors.rs - Stable value-slot identity registry
├── nodes.rs - Node record storage and subtree extraction
├── anchors.rs - AnchorRegistry and anchor state tracking
├── scope_index.rs - ScopeId -> active group lookup
├── detach.rs - DetachedSubtree extraction and restore
├── validate/ - Structural invariant checking
├── debug.rs / reader.rs - Debug snapshots and textual dumps
└── lifecycle.rs - Deferred payload disposal
Data Structures
SlotTable
The active table stores groups, payloads, nodes, stable identity registries, and scope lookup separately:
Key properties:
- Exact active structure:
subtree_lenalways means the active preorder span. - Stable addressing: groups use stable
AnchorIdidentities plus transientActiveGroupIdhandles; values use anchor-basedValueSlotId. - Indexed scopes: active scopes map directly to group anchors.
- Explicit retention: removed branches are detached from the table before any retain/dispose decision happens.
GroupRecord
Each group describes one active composable call:
parent_anchor stays stable even when active indexes shift. subtree_len and
subtree_node_count are validated against the actual preorder tree.
PayloadRecord and NodeRecord
Remembered values and emitted nodes are stored outside the structural group table:
Payload anchors back ValueSlotId, and ValueSlotId also stores the owning
slot-table storage id. Remembered values remain addressable when sibling
reordering moves the owning group in the active table, while stale cross-table
handles fail instead of aliasing another table.
DetachedSubtree
When a branch leaves the active table, storage returns an owned subtree:
Detached subtrees carry remembered payloads, anchors, scope IDs, and node identities together. The slot table itself does not decide whether they are retained or disposed.
Core Operations
begin_group() - Begin Group
Writers match groups only among siblings of the current parent:
There is no fixed global search budget and no recursive rescue scan into grandchildren. Large sibling ranges build a temporary sibling index inside the active writer frame.
finish_group_body() / end_group()
At the end of a group body, the writer trims payloads and direct nodes that were not visited, detaches unvisited child subtrees, and returns them for retain-or-dispose handling:
let finish = slots.finish_group_body;
composer.handle_detached_children;
slots.end_group;
This is where inactive branches become DetachedSubtree values. They are no longer present in
the active table after finish_group_body.
value_slot() / remember()
Remembered state is stored in the payload table and addressed by ValueSlotId:
let slot = slots.value_slot;
let state = slots.;
If a payload slot is revisited with a different type, the old boxed value is dropped through the slot lifecycle coordinator and the new typed payload replaces it in place. Public composer-held value access uses typed value-slot handles; the old untyped composer write surface is not part of the active API.
begin_recompose_at_scope()
Targeted recomposition starts from the indexed scope mapping:
if let Some = slots.begin_recompose_at_scope
Detached scopes are intentionally absent from that index. They stay inactive until their retained subtree is explicitly restored.
Active-Table Invariants
The slot table validates a small set of invariants after mutations in debug/test builds:
- active groups form one valid preorder forest;
- every
subtree_lenandsubtree_node_countmatches the actual active subtree; - payload and node ranges are contiguous and owner-correct;
- every active scope index entry resolves to the correct group anchor;
- retained subtree anchors are detached or invalidated, never active.
- debug stats report occupied invalidated anchor slots separately from reusable free anchor IDs for both group and payload anchor registries.
The active debug helpers are SlotTable::validate(), SlotTable::debug_snapshot(),
SlotTable::debug_dump_groups(), and SlotTable::debug_dump_slot_entries().
Integration Points
The snapshot and slot table systems integrate at several key points:
1. State Storage in Slots
State objects are stored in slot-table payload records and accessed through composer helpers:
composer.with_group;
Key insight: Slot table manages where state is stored, snapshots manage what values are visible.
2. Scope-Based Recomposition
Scopes are attached to groups during composition and later resolved through the active scope index:
let started = slots.begin_group;
slots.set_group_scope;
// Later:
slots.begin_recompose_at_scope;
Snapshot integration:
// Snapshot observer tracks which scopes read which state
let observer = new;
snapshot.set_read_observer;
Flow:
- Composition reads state → observer records
(scope, state_obj)mapping - State changes → observer invalidates affected scopes
- Active scope index resolves the group anchor → recompose at that group
Detached scopes are intentionally absent from the active scope index. When a retained subtree is restored, its scope is reactivated and can re-enter normal recomposition.
3. Invalidation Tracking
Recomposition flow:
// 1. Apply snapshot changes
snapshot.apply?;
// 2. Get invalidated scopes
let invalid_scopes = observer.notify_changed_objects;
// 3. Recompose each scope
for scope in invalid_scopes
4. Composition Context
The composition runtime coordinates slot passes, command application, and retention policy:
composition.render;
// inside:
// - SlotsHost begins a compose/recompose pass
// - SlotWriteSession mutates the active SlotTable
// - detached children become DetachedSubtree values
// - composer/host decide retain vs dispose
// - command queue applies node attach/move/remove work to the applier
Key Algorithms
Three-Way Merge Algorithm
Used when applying snapshots with concurrent modifications:
Example merges:
Structural equality (integers):
previous: 10
current: 15 (another snapshot wrote this)
applied: 20 (our snapshot wrote this)
merge: Cannot merge conflicting integers → Conflict
Set merge (additive):
previous: {A, B}
current: {A, B, C} (another snapshot added C)
applied: {A, B, D} (our snapshot added D)
merge: {A, B, C, D} (union) → CommitMerged
List merge (operational transform):
previous: ["a", "b", "c"]
current: ["a", "x", "b", "c"] (inserted "x" at 1)
applied: ["a", "b", "c", "y"] (appended "y")
merge: ["a", "x", "b", "c", "y"] (apply both ops) → CommitMerged
Sibling Matching and Movement
The writer only matches direct siblings under the current parent:
Why it matters:
- the search is parent-bounded;
- grandchildren are never treated as sibling matches;
- retention is not mixed into sibling search;
- the semantics depend on exact keys, not rescue budgets.
Detach and Restore
Conditional structure changes become explicit subtree extraction and reinsertion:
Why it matters:
- removed structure is no longer present in the active table;
- retained state is explicit and owner-controlled;
- restoring a retained subtree preserves remembered payloads, scopes, and node identities.
Design Patterns
Persistent/Immutable Data Structures (NOT True CoW)
Used in: SnapshotIdSet
IMPORTANT CLARIFICATION: The documentation claims "CoW" but this is NOT true copy-on-write. It's actually a persistent/immutable data structure with partial optimization.
Actual Implementation:
Usage Pattern (3 clones per snapshot!):
// global.rs:141-143
let mut parent_invalid = self.state.invalid.borrow.clone; // Clone 1
parent_invalid = parent_invalid.set; // Clone 2
self.state.invalid.replace; // Clone 3
What's Actually Happening:
- ✅ Immutable: Can't modify in place (functional correctness)
- ✅ Fast for recent IDs: Only bit operations, no allocation
- ❌ NOT true CoW:
Box::clone()does full array copy - ❌ Suboptimal: Could use
Arc<[SnapshotId]>for O(1) sharing
True CoW Would Be:
below_bound: , // Share via Arc
// .clone() would just bump refcount, no array copy
Real-World Impact:
- Best case (all recent IDs): 24 bytes copied, very fast ✅
- Worst case (100 old IDs): ~2.4 KB copied per snapshot creation ⚠️
- Not dead code: Works correctly, just not optimally
Why It Works Despite Not Being True CoW:
- Most snapshot IDs are recent (fit in bit sets)
below_boundarray is typically small or empty- Correctness > performance (for now)
Object Pool
Used in: StateRecord reuse
Pattern:
// Mark object as reusable
record.snapshot_id.set;
// Reuse later
if record.snapshot_id.get == INVALID_SNAPSHOT
Benefits:
- Reduces allocation pressure
- Maintains stable Arc pointers
- Amortizes allocation cost
Observer Pattern
Used in: Snapshot read/write tracking, invalidation
Pattern:
// Usage
snapshot.set_read_observer;
Benefits:
- Decouple observation from core logic
- Enable multiple observation strategies
- Support transparent snapshots
Strategy Pattern
Used in: MutationPolicy, slot write sessions
Pattern:
// Implementations
;
;
;
Benefits:
- Customize state comparison logic
- Different merge strategies per type
- Extensible without modifying core
Writer Frame Pattern
Used in: Slot table composition and recomposition sessions
Pattern:
pub
pub
Benefits:
- Traversal state is scoped to the active writer session instead of living inside table storage
- Group reuse, movement, and restoration operate against sibling lists and anchors
- Composition code works through semantic
begin_group/finish_group_body/end_groupoperations
Performance Characteristics
Snapshot System
| Operation | Time Complexity | Notes |
|---|---|---|
| Create snapshot | O(1) amortized | Allocate ID, copy open set |
| Read state | O(R) | R = record chain length, typically small |
| Write state | O(1) amortized | Reuse or prepend record |
| Apply snapshot | O(M × R) | M = modified objects, R = record chain |
| GC record cleanup | O(R) | Per state object |
| SnapshotIdSet get | O(1) recent, O(log N) old | Recent = last 128 IDs |
| SnapshotIdSet set | O(1) recent, O(N) old | Copy-on-write |
Optimization opportunities:
- Keep record chains short via aggressive GC
- Use read-only snapshots when possible (no tracking overhead)
- Batch apply operations
- Use ReferentialEqualityPolicy for cheap equality checks
Slot Table System
| Operation | Time Complexity | Notes |
|---|---|---|
begin_group() reuse |
O(1) | Expected sibling already matches |
begin_group() sibling search |
O(D) | D = number of direct siblings examined |
move_subtree() |
O(G + P + N + T) | moved groups, payloads, nodes, and suffix metadata shifts |
finish_group_body() |
O(R + C) | trims direct payload/node tails and detaches remaining child subtrees |
detach_subtree() |
O(G + P + N + T) | extracts subtree records and repairs active indexes |
restore_subtree() |
O(G + P + N + T) | reinserts subtree records and recomputes metadata |
value_slot() / read_value() |
O(1) | payload anchor lookup plus owner-relative offset |
begin_recompose_at_scope() |
O(1) | scope index -> anchor -> active group |
validate() |
O(G + P + N + A + S) | full structural check in debug/test builds |
Optimization opportunities:
- Reduce temporary
Veccloning in detach/retention hot paths - Profile subtree
Vec::drain/Vec::splicecosts before changing storage layout - Add retained-memory instrumentation before pursuing more complex backends
- Keep validation strong while optimizing
Memory Usage
Snapshot system:
- Each StateRecord: ~64 bytes (Arc, Cell, RwLock overhead)
- SnapshotIdSet: 24 bytes + 8 bytes per old ID
- MutableSnapshot: ~200 bytes + modified map
Slot table:
- Group storage, payload storage, and node storage scale independently
- Anchor and scope indexes add hash-map overhead on top of active records
- Retained branches consume separate detached subtree allocations while inactive
Scaling:
- 10,000 UI elements no longer imply one flat slot array
- 1,000 state objects with 10 records each: ~640 KB
- Typical app: 1-10 MB for composition runtime
Common Scenarios
Scenario 1: Simple State Update
// 1. Create state
let state = new;
// 2. Read in current snapshot
let value = state.read; // Returns 0
// 3. Create mutable snapshot
let snapshot = take_mutable_snapshot;
// 4. Write in snapshot
state.write;
// 5. Apply snapshot
snapshot.apply?; // Merge into global
// 6. Read new value
let new_value = state.read; // Returns 42
Internals:
- State has single record:
[id=1, value=0] - Write creates new record:
[id=2, value=42] → [id=1, value=0] - Apply promotes child record to global visibility
- Global snapshot now sees
id=2as valid
Scenario 2: Conditional Rendering (Tabs)
// Initial composition - Tab 1 active
composer.with_group;
// Hide Tab 1
composer.with_group;
// Result:
// - finish_group_body() detaches Tab 1 as DetachedSubtree
// - retain policy keeps it outside the active SlotTable
// - its remembered payloads, scopes, and node IDs stay owned by the detached subtree
// Show Tab 1 again
composer.with_group;
Key benefit: Tab 1's state is preserved by an explicit retained subtree, not by hidden gap metadata in the active table.
Scenario 3: Concurrent Snapshot Conflict
// t0: Initial state
let state = new;
// t1: Create two snapshots
let snapshot1 = take_mutable_snapshot; // base_parent_id = 1
let snapshot2 = take_mutable_snapshot; // base_parent_id = 1
// t2: Snapshot1 writes
state.write;
// t3: Snapshot2 writes (concurrent)
state.write;
// t4: Snapshot1 applies first
snapshot1.apply?; // Success, now global = 20
// t5: Snapshot2 tries to apply
let result = snapshot2.apply;
// Conflict detected: last_write (snapshot1) != base_parent_id (global before)
// Merge attempt: previous=10, current=20, applied=30
// StructuralEqualityPolicy cannot merge integers
// Result: SnapshotApplyResult::Failure
Handling conflicts:
loop
Scenario 4: Nested Snapshots (Transaction-like)
let outer = take_mutable_snapshot;
// Modify state
state1.write;
// Outer can now see inner's changes
assert_eq!;
// Apply outer to global
outer.apply?;
// Both changes now visible globally
assert_eq!;
assert_eq!;
Use case: Atomic multi-state updates, rollback on failure.
Future Optimizations
Snapshot System
- Persistent Data Structures: Replace record chains with persistent trees for O(log N) all operations
- Lock-Free Records: Use atomic operations instead of RwLock for high-contention scenarios
- Compressed ID Sets: Use roaring bitmaps for very large snapshot ID sets
- Lazy GC: Defer record cleanup to background thread
Slot Table System
- Chunked subtree storage: Replace hot
Vec::drain/Vec::splicepaths only if profiling proves they dominate. - Denser group storage: Pack
GroupRecordfields or split arrays only if cache pressure matters in real traces. - Retained-memory diagnostics: Add stronger retained-subtree and anchor-capacity telemetry for leak hunting.
- Parallel recomposition: Only after current invariants, retention semantics, and applier-side node ownership stay deterministic.
Debugging Tips
Snapshot Debugging
View record chain:
Check snapshot visibility:
Slot Table Debugging
Dump the active slot table:
Inspect structured debug state:
Force validation in debug/test paths:
assert_eq!;
Enable automatic pass dumps:
COMPOSE_DEBUG_SLOT_TABLE=1
Summary
The Snapshots and Slot Table system provides a sophisticated foundation for cranpose:
Snapshots deliver:
- Isolated state views via MVCC
- Optimistic concurrency with conflict detection
- Flexible merge strategies
- Efficient garbage collection
Slot Tables deliver:
- Exact active-tree storage with separate payload and node tables
- Stable anchors and indexed scopes for targeted recomposition
- Explicit detached-subtree retention instead of semantic gaps
- Strong validation and debug snapshots for structural correctness
Together they enable:
- Predictable recomposition
- State preservation across structural changes
- Concurrent snapshot modifications
- High-performance UI updates
This design mirrors Jetpack Compose's battle-tested architecture while leveraging Rust's ownership model for memory safety and performance.