Skip to main content

cranpose_core/
snapshot_pinning.rs

1use std::cell::RefCell;
2
3/// Snapshot pinning system to prevent premature garbage collection of state records.
4///
5/// This module implements a pinning table that tracks which snapshot IDs need to remain
6/// alive. When a snapshot is created, it "pins" the lowest snapshot ID that it depends on,
7/// preventing state records from those snapshots from being garbage collected.
8///
9/// Uses SnapshotDoubleIndexHeap for O(log N) pin/unpin and O(1) lowest queries.
10/// Based on Jetpack Compose's pinning mechanism (Snapshot.kt:714-722, 1954).
11use crate::snapshot_double_index_heap::SnapshotDoubleIndexHeap;
12use crate::{
13    snapshot_double_index_heap::SnapshotDoubleIndexHeapDebugStats,
14    snapshot_id_set::{SnapshotId, SnapshotIdSet},
15};
16
17/// A handle to a pinned snapshot. Dropping this handle releases the pin.
18///
19/// Internally stores a heap handle for O(log N) removal.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct PinHandle(usize);
22
23impl PinHandle {
24    /// Invalid pin handle constant (0 is reserved as invalid).
25    pub const INVALID: PinHandle = PinHandle(0);
26
27    /// Check if this handle is valid (non-zero).
28    pub fn is_valid(&self) -> bool {
29        self.0 != 0
30    }
31}
32
33/// The global pinning table that tracks pinned snapshots using a min-heap.
34struct PinningTable {
35    /// Min-heap of pinned snapshot IDs for O(1) lowest queries
36    heap: SnapshotDoubleIndexHeap,
37}
38
39impl PinningTable {
40    fn new() -> Self {
41        Self {
42            heap: SnapshotDoubleIndexHeap::new(),
43        }
44    }
45
46    /// Add a pin for the given snapshot ID, returning a handle.
47    ///
48    /// Time complexity: O(log N)
49    fn add(&mut self, snapshot_id: SnapshotId) -> PinHandle {
50        let heap_handle = self.heap.add(snapshot_id);
51        // Heap handles start at 0, but we reserve 0 as INVALID for PinHandle
52        // So we offset by 1: heap handle 0 → PinHandle(1), etc.
53        PinHandle(heap_handle + 1)
54    }
55
56    /// Remove a pin by handle.
57    ///
58    /// Time complexity: O(log N)
59    fn remove(&mut self, handle: PinHandle) -> bool {
60        if !handle.is_valid() {
61            return false;
62        }
63
64        // Convert PinHandle back to heap handle (subtract 1)
65        let heap_handle = handle.0 - 1;
66
67        // Verify handle is within bounds
68        if heap_handle < usize::MAX {
69            self.heap.remove(heap_handle);
70            true
71        } else {
72            false
73        }
74    }
75
76    /// Get the lowest pinned snapshot ID, or None if nothing is pinned.
77    ///
78    /// Time complexity: O(1)
79    fn lowest_pinned(&self) -> Option<SnapshotId> {
80        if self.heap.is_empty() {
81            None
82        } else {
83            // Use 0 as default (will never be returned since heap is non-empty)
84            Some(self.heap.lowest_or_default(0))
85        }
86    }
87
88    /// Get the count of pins (for testing/debugging).
89    fn pin_count(&self) -> usize {
90        self.heap.len()
91    }
92
93    fn debug_stats(&self) -> SnapshotPinningDebugStats {
94        SnapshotPinningDebugStats {
95            pin_count: self.pin_count(),
96            lowest_pinned_snapshot: self.lowest_pinned(),
97            heap: self.heap.debug_stats(),
98        }
99    }
100}
101
102thread_local! {
103    // Global pinning table protected by a mutex.
104    static PINNING_TABLE: RefCell<PinningTable> = RefCell::new(PinningTable::new());
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
108pub struct SnapshotPinningDebugStats {
109    pub pin_count: usize,
110    pub lowest_pinned_snapshot: Option<SnapshotId>,
111    pub heap: SnapshotDoubleIndexHeapDebugStats,
112}
113
114/// Pin a snapshot and its invalid set, returning a handle.
115///
116/// This should be called when a snapshot is created to ensure that state records
117/// from the pinned snapshot and all its dependencies remain valid.
118///
119/// # Arguments
120/// * `snapshot_id` - The ID of the snapshot being created
121/// * `invalid` - The set of invalid snapshot IDs for this snapshot
122///
123/// # Returns
124/// A pin handle that should be released when the snapshot is disposed.
125///
126/// # Time Complexity
127/// O(log N) where N is the number of pinned snapshots
128pub fn track_pinning(snapshot_id: SnapshotId, invalid: &SnapshotIdSet) -> PinHandle {
129    // Pin the lowest snapshot ID that this snapshot depends on
130    let pinned_id = invalid.lowest(snapshot_id);
131
132    PINNING_TABLE.with(|cell| cell.borrow_mut().add(pinned_id))
133}
134
135/// Release a pinned snapshot.
136///
137/// # Arguments
138/// * `handle` - The pin handle returned by `track_pinning`
139///
140/// This must be called while holding the appropriate lock (sync).
141///
142/// # Time Complexity
143/// O(log N) where N is the number of pinned snapshots
144pub fn release_pinning(handle: PinHandle) {
145    if !handle.is_valid() {
146        return;
147    }
148
149    PINNING_TABLE.with(|cell| {
150        cell.borrow_mut().remove(handle);
151    });
152}
153
154/// Get the lowest currently pinned snapshot ID.
155///
156/// This is used to determine which state records can be safely garbage collected.
157/// Any state records from snapshots older than this ID are still potentially in use.
158///
159/// # Time Complexity
160/// O(1)
161pub fn lowest_pinned_snapshot() -> Option<SnapshotId> {
162    PINNING_TABLE.with(|cell| cell.borrow().lowest_pinned())
163}
164
165/// Get the current count of pinned snapshots (for testing).
166/// Get the current count of pinned snapshots (for testing/debugging).
167pub fn pin_count() -> usize {
168    PINNING_TABLE.with(|cell| cell.borrow().pin_count())
169}
170
171pub fn debug_snapshot_pinning_stats() -> SnapshotPinningDebugStats {
172    PINNING_TABLE.with(|cell| cell.borrow().debug_stats())
173}
174
175/// Reset the pinning table (for testing).
176#[cfg(test)]
177pub fn reset_pinning_table() {
178    PINNING_TABLE.with(|cell| {
179        let mut table = cell.borrow_mut();
180        table.heap = SnapshotDoubleIndexHeap::new();
181    });
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    // Helper to ensure tests start with clean state
189    fn setup() {
190        reset_pinning_table();
191    }
192
193    #[test]
194    fn test_invalid_handle() {
195        let handle = PinHandle::INVALID;
196        assert!(!handle.is_valid());
197        assert_eq!(handle.0, 0);
198    }
199
200    #[test]
201    fn test_valid_handle() {
202        setup();
203        let invalid = SnapshotIdSet::new().set(10);
204        let handle = track_pinning(20, &invalid);
205        assert!(handle.is_valid());
206        assert!(handle.0 > 0);
207    }
208
209    #[test]
210    fn test_track_and_release() {
211        setup();
212
213        let invalid = SnapshotIdSet::new().set(10);
214        let handle = track_pinning(20, &invalid);
215
216        assert_eq!(pin_count(), 1);
217        assert_eq!(lowest_pinned_snapshot(), Some(10));
218
219        release_pinning(handle);
220        assert_eq!(pin_count(), 0);
221        assert_eq!(lowest_pinned_snapshot(), None);
222    }
223
224    #[test]
225    fn test_multiple_pins() {
226        setup();
227
228        let invalid1 = SnapshotIdSet::new().set(10);
229        let handle1 = track_pinning(20, &invalid1);
230
231        let invalid2 = SnapshotIdSet::new().set(5).set(15);
232        let handle2 = track_pinning(30, &invalid2);
233
234        assert_eq!(pin_count(), 2);
235        assert_eq!(lowest_pinned_snapshot(), Some(5));
236
237        // Release first pin
238        release_pinning(handle1);
239        assert_eq!(pin_count(), 1);
240        assert_eq!(lowest_pinned_snapshot(), Some(5));
241
242        // Release second pin
243        release_pinning(handle2);
244        assert_eq!(pin_count(), 0);
245        assert_eq!(lowest_pinned_snapshot(), None);
246    }
247
248    #[test]
249    fn test_duplicate_pins() {
250        setup();
251
252        // Pin the same snapshot ID twice
253        let invalid = SnapshotIdSet::new().set(10);
254        let handle1 = track_pinning(20, &invalid);
255        let handle2 = track_pinning(25, &invalid);
256
257        assert_eq!(pin_count(), 2);
258        assert_eq!(lowest_pinned_snapshot(), Some(10));
259
260        // Releasing one doesn't unpin completely
261        release_pinning(handle1);
262        assert_eq!(pin_count(), 1);
263        assert_eq!(lowest_pinned_snapshot(), Some(10));
264
265        // Releasing second one unpins completely
266        release_pinning(handle2);
267        assert_eq!(pin_count(), 0);
268        assert_eq!(lowest_pinned_snapshot(), None);
269    }
270
271    #[test]
272    fn test_pin_ordering() {
273        setup();
274
275        // Add pins in non-sorted order
276        let invalid1 = SnapshotIdSet::new().set(30);
277        let _handle1 = track_pinning(40, &invalid1);
278
279        let invalid2 = SnapshotIdSet::new().set(10);
280        let _handle2 = track_pinning(20, &invalid2);
281
282        let invalid3 = SnapshotIdSet::new().set(20);
283        let _handle3 = track_pinning(30, &invalid3);
284
285        // Lowest should still be 10
286        assert_eq!(lowest_pinned_snapshot(), Some(10));
287    }
288
289    #[test]
290    fn test_release_invalid_handle() {
291        setup();
292
293        // Releasing an invalid handle should not crash
294        release_pinning(PinHandle::INVALID);
295        assert_eq!(pin_count(), 0);
296    }
297
298    #[test]
299    fn test_empty_invalid_set() {
300        setup();
301
302        // Empty invalid set means snapshot depends on nothing older
303        let invalid = SnapshotIdSet::new();
304        let handle = track_pinning(100, &invalid);
305
306        // Should pin snapshot 100 itself (lowest returns the upper bound if empty)
307        assert_eq!(pin_count(), 1);
308        assert_eq!(lowest_pinned_snapshot(), Some(100));
309
310        release_pinning(handle);
311    }
312
313    #[test]
314    fn test_lowest_from_invalid_set() {
315        setup();
316
317        // Create an invalid set with multiple IDs
318        let invalid = SnapshotIdSet::new().set(5).set(10).set(15).set(20);
319        let handle = track_pinning(25, &invalid);
320
321        // Should pin the lowest ID from the invalid set
322        assert_eq!(lowest_pinned_snapshot(), Some(5));
323
324        release_pinning(handle);
325    }
326
327    #[test]
328    fn test_concurrent_snapshots() {
329        setup();
330
331        // Simulate multiple concurrent snapshots
332        let handles: Vec<_> = (0..10)
333            .map(|i| {
334                let invalid = SnapshotIdSet::new().set(i * 10);
335                track_pinning(i * 10 + 5, &invalid)
336            })
337            .collect();
338
339        assert_eq!(pin_count(), 10);
340        assert_eq!(lowest_pinned_snapshot(), Some(0));
341
342        // Release all
343        for handle in handles {
344            release_pinning(handle);
345        }
346
347        assert_eq!(pin_count(), 0);
348        assert_eq!(lowest_pinned_snapshot(), None);
349    }
350
351    #[test]
352    fn test_heap_handle_based_removal() {
353        setup();
354
355        // Test that we can remove pins using just the handle, without knowing the snapshot ID
356        let invalid1 = SnapshotIdSet::new().set(42);
357        let invalid2 = SnapshotIdSet::new().set(17);
358        let invalid3 = SnapshotIdSet::new().set(99);
359
360        let h1 = track_pinning(50, &invalid1);
361        let h2 = track_pinning(25, &invalid2);
362        let h3 = track_pinning(100, &invalid3);
363
364        assert_eq!(pin_count(), 3);
365        assert_eq!(lowest_pinned_snapshot(), Some(17));
366
367        // Remove middle value using only handle
368        release_pinning(h1);
369        assert_eq!(pin_count(), 2);
370        assert_eq!(lowest_pinned_snapshot(), Some(17));
371
372        // Remove lowest using only handle
373        release_pinning(h2);
374        assert_eq!(pin_count(), 1);
375        assert_eq!(lowest_pinned_snapshot(), Some(99));
376
377        release_pinning(h3);
378        assert!(pin_count() == 0);
379    }
380}