Skip to main content

scirs2_core/memory/
defrag.rs

1//! Memory defragmentation for long-running GPU/CPU workloads.
2//!
3//! Implements compaction of fragmented memory pools using a planner/executor
4//! separation so callers can apply moves to their own backing store.
5//!
6//! # Design
7//!
8//! The `DefragPlanner` tracks allocated and free blocks by offset, then
9//! computes a compaction plan — a list of `(from_offset, to_offset, size)`
10//! moves that pack all allocations toward offset 0.  The caller can then
11//! either:
12//!
13//! - Execute the plan on a flat `Vec<u8>` with `execute_compaction`.
14//! - Apply the moves to their own GPU buffer using the plan directly.
15//!
16//! The `OnlineDefragmenter` wraps the planner with a threshold-based trigger
17//! so callers can simply call `on_alloc` / `on_free` and let the defragmenter
18//! decide when to compact.
19//!
20//! # Example
21//!
22//! ```rust
23//! use scirs2_core::memory::defrag::DefragPlanner;
24//!
25//! let mut planner = DefragPlanner::new(1024);
26//! planner.record_alloc(0, 256);
27//! // leave a hole at offset 256 (no record_alloc there)
28//! planner.record_free(256, 256);   // hole in the middle
29//! planner.record_alloc(512, 256);
30//!
31//! let moves = planner.plan_compaction();
32//! // moves will contain: move block at 512 -> 256
33//! assert!(!moves.is_empty());
34//! ```
35
36use std::collections::BTreeMap;
37
38// ---------------------------------------------------------------------------
39// FreeBlock
40// ---------------------------------------------------------------------------
41
42/// A free block record: contiguous free region starting at `offset`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct FreeBlock {
45    /// Byte offset from pool start.
46    pub offset: usize,
47    /// Size in bytes.
48    pub size: usize,
49}
50
51// ---------------------------------------------------------------------------
52// DefragStats
53// ---------------------------------------------------------------------------
54
55/// Statistics from a defragmentation pass.
56#[derive(Debug, Clone, Default)]
57pub struct DefragStats {
58    /// Number of allocated blocks that were relocated.
59    pub blocks_moved: usize,
60    /// Total bytes relocated.
61    pub bytes_compacted: usize,
62    /// Fragmentation ratio before compaction (0.0 = compact, 1.0 = fully fragmented).
63    pub fragmentation_before: f64,
64    /// Fragmentation ratio after compaction.
65    pub fragmentation_after: f64,
66}
67
68// ---------------------------------------------------------------------------
69// DefragPlanner
70// ---------------------------------------------------------------------------
71
72/// Defragmentation planner: computes move operations to compact a pool.
73///
74/// The planner operates on logical block metadata only; it does not own any
75/// memory itself.  Callers register their current allocation layout, then
76/// call `plan_compaction()` to receive a move list.
77pub struct DefragPlanner {
78    total_capacity: usize,
79    /// offset -> size for currently allocated blocks.
80    allocated_blocks: BTreeMap<usize, usize>,
81    /// Free blocks (not necessarily sorted; use `coalesce_free_blocks` to merge).
82    free_blocks: Vec<FreeBlock>,
83}
84
85impl DefragPlanner {
86    /// Create a new planner for a pool of `total_capacity` bytes.
87    pub fn new(total_capacity: usize) -> Self {
88        Self {
89            total_capacity,
90            allocated_blocks: BTreeMap::new(),
91            free_blocks: Vec::new(),
92        }
93    }
94
95    /// Record an allocated block at `offset` with the given `size`.
96    pub fn record_alloc(&mut self, offset: usize, size: usize) {
97        self.allocated_blocks.insert(offset, size);
98    }
99
100    /// Record a free block at `offset` with the given `size`.
101    pub fn record_free(&mut self, offset: usize, size: usize) {
102        self.free_blocks.push(FreeBlock { offset, size });
103    }
104
105    /// Compute the fragmentation ratio.
106    ///
107    /// Returns `(total_free - largest_contiguous_free) / total_free`.
108    /// Returns `0.0` when there are no free blocks.
109    pub fn fragmentation_ratio(&self) -> f64 {
110        let total_free: usize = self.free_blocks.iter().map(|b| b.size).sum();
111        if total_free == 0 {
112            return 0.0;
113        }
114        let largest = self.free_blocks.iter().map(|b| b.size).max().unwrap_or(0);
115        let non_contiguous = total_free.saturating_sub(largest);
116        non_contiguous as f64 / total_free as f64
117    }
118
119    /// Plan defragmentation: returns `(from_offset, to_offset, size)` moves
120    /// that compact all allocations toward offset 0.
121    ///
122    /// The returned moves are in application order; later moves may depend on
123    /// earlier ones, so apply them in sequence.
124    pub fn plan_compaction(&self) -> Vec<(usize, usize, usize)> {
125        let mut moves = Vec::new();
126        let mut write_cursor: usize = 0;
127
128        // Iterate over allocated blocks in ascending offset order.
129        for (&offset, &size) in &self.allocated_blocks {
130            if offset != write_cursor {
131                // This block needs to move earlier.
132                moves.push((offset, write_cursor, size));
133            }
134            write_cursor += size;
135        }
136        moves
137    }
138
139    /// Execute the compaction plan on a flat byte buffer in place.
140    ///
141    /// Returns statistics describing what was done.
142    /// The buffer must be exactly `total_capacity` bytes long.
143    pub fn execute_compaction(&self, buffer: &mut Vec<u8>) -> DefragStats {
144        let frag_before = self.fragmentation_ratio();
145
146        if buffer.len() < self.total_capacity {
147            buffer.resize(self.total_capacity, 0);
148        }
149
150        let moves = self.plan_compaction();
151        let blocks_moved = moves.len();
152        let bytes_compacted: usize = moves.iter().map(|(_, _, s)| s).sum();
153
154        for (from, to, size) in &moves {
155            // Use copy_within for non-overlapping or safe memmove semantics.
156            buffer.copy_within(*from..*from + *size, *to);
157        }
158
159        // After compaction the free area is at the end.
160        let _allocated_total: usize = self.allocated_blocks.values().sum();
161        // After compaction there is a single contiguous free block at the end,
162        // so fragmentation is always 0.0 regardless of the remaining capacity.
163        let frag_after = 0.0_f64;
164
165        DefragStats {
166            blocks_moved,
167            bytes_compacted,
168            fragmentation_before: frag_before,
169            fragmentation_after: frag_after,
170        }
171    }
172
173    /// Merge adjacent free blocks (coalescing).
174    ///
175    /// Returns the number of merges performed.
176    pub fn coalesce_free_blocks(&mut self) -> usize {
177        if self.free_blocks.is_empty() {
178            return 0;
179        }
180
181        // Sort by offset.
182        self.free_blocks.sort_by_key(|b| b.offset);
183
184        let mut merged: Vec<FreeBlock> = Vec::with_capacity(self.free_blocks.len());
185        let mut merge_count = 0usize;
186
187        let mut current = self.free_blocks[0];
188        for &block in &self.free_blocks[1..] {
189            if current.offset + current.size == block.offset {
190                // Adjacent: extend current.
191                current.size += block.size;
192                merge_count += 1;
193            } else {
194                merged.push(current);
195                current = block;
196            }
197        }
198        merged.push(current);
199
200        self.free_blocks = merged;
201        merge_count
202    }
203
204    /// Total capacity of the pool.
205    pub fn total_capacity(&self) -> usize {
206        self.total_capacity
207    }
208
209    /// Number of currently tracked allocated blocks.
210    pub fn allocated_block_count(&self) -> usize {
211        self.allocated_blocks.len()
212    }
213
214    /// Number of currently tracked free blocks.
215    pub fn free_block_count(&self) -> usize {
216        self.free_blocks.len()
217    }
218
219    /// Total bytes in allocated blocks.
220    pub fn allocated_bytes(&self) -> usize {
221        self.allocated_blocks.values().sum()
222    }
223}
224
225// ---------------------------------------------------------------------------
226// OnlineDefragmenter
227// ---------------------------------------------------------------------------
228
229/// Online defragmenter that wraps `DefragPlanner` with threshold-based compaction.
230///
231/// After each `on_alloc` / `on_free` call the fragmentation ratio is checked.
232/// When it exceeds `threshold`, `compact()` may be called to apply compaction.
233pub struct OnlineDefragmenter {
234    planner: DefragPlanner,
235    threshold: f64,
236    compaction_count: usize,
237}
238
239impl OnlineDefragmenter {
240    /// Create a new `OnlineDefragmenter` for a pool of `capacity` bytes.
241    ///
242    /// `threshold`: fragmentation ratio [0.0, 1.0] above which compaction is
243    /// recommended.  A value of `0.5` means compact when > 50 % of free space
244    /// is fragmented.
245    pub fn new(capacity: usize, threshold: f64) -> Self {
246        Self {
247            planner: DefragPlanner::new(capacity),
248            threshold: threshold.clamp(0.0, 1.0),
249            compaction_count: 0,
250        }
251    }
252
253    /// Notify the defragmenter that an allocation at `offset` of `size` bytes
254    /// was made.
255    pub fn on_alloc(&mut self, offset: usize, size: usize) {
256        self.planner.record_alloc(offset, size);
257    }
258
259    /// Notify the defragmenter that `size` bytes at `offset` were freed.
260    pub fn on_free(&mut self, offset: usize, size: usize) {
261        self.planner.record_free(offset, size);
262    }
263
264    /// Returns `true` if the current fragmentation ratio exceeds the threshold.
265    pub fn should_compact(&self) -> bool {
266        self.planner.fragmentation_ratio() > self.threshold
267    }
268
269    /// Apply compaction to `buffer` and update internal state.
270    ///
271    /// After compaction the free blocks are reset to a single tail block.
272    pub fn compact(&mut self, buffer: &mut Vec<u8>) -> DefragStats {
273        let stats = self.planner.execute_compaction(buffer);
274        self.compaction_count += 1;
275
276        // Rebuild planner state: all allocations are now packed from offset 0.
277        let allocated_total = self.planner.allocated_bytes();
278        let capacity = self.planner.total_capacity();
279        let mut packed: BTreeMap<usize, usize> = BTreeMap::new();
280        let mut cursor = 0usize;
281        for &size in self.planner.allocated_blocks.values() {
282            packed.insert(cursor, size);
283            cursor += size;
284        }
285        self.planner.allocated_blocks = packed;
286        self.planner.free_blocks.clear();
287        if allocated_total < capacity {
288            self.planner.free_blocks.push(FreeBlock {
289                offset: allocated_total,
290                size: capacity - allocated_total,
291            });
292        }
293
294        stats
295    }
296
297    /// Number of compaction operations performed so far.
298    pub fn compaction_count(&self) -> usize {
299        self.compaction_count
300    }
301
302    /// Current fragmentation ratio.
303    pub fn fragmentation_ratio(&self) -> f64 {
304        self.planner.fragmentation_ratio()
305    }
306
307    /// Reference to the inner planner (read-only).
308    pub fn planner(&self) -> &DefragPlanner {
309        &self.planner
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Tests
315// ---------------------------------------------------------------------------
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_defrag_planner_basic() {
323        // Layout: [A:256][B:256][A:256] with B freed => fragmentation
324        let mut planner = DefragPlanner::new(768);
325        planner.record_alloc(0, 256);
326        planner.record_alloc(256, 256);
327        planner.record_alloc(512, 256);
328
329        // Free the middle block.
330        planner.record_free(256, 256);
331
332        let ratio = planner.fragmentation_ratio();
333        // Only one free block, so largest == total_free => ratio = 0.0.
334        // (no fragmentation when there's exactly one free region)
335        assert_eq!(ratio, 0.0, "single free block => no fragmentation");
336
337        // Add a second disconnected free block to create fragmentation.
338        planner.record_free(0, 128);
339        let ratio2 = planner.fragmentation_ratio();
340        assert!(
341            ratio2 > 0.0,
342            "two non-adjacent free blocks => fragmentation > 0"
343        );
344    }
345
346    #[test]
347    fn test_defrag_coalesce() {
348        let mut planner = DefragPlanner::new(1024);
349        // Two adjacent free blocks.
350        planner.record_free(0, 256);
351        planner.record_free(256, 256);
352
353        let merges = planner.coalesce_free_blocks();
354        assert_eq!(merges, 1, "exactly one merge expected");
355        assert_eq!(
356            planner.free_block_count(),
357            1,
358            "should have collapsed to 1 block"
359        );
360        assert_eq!(planner.free_blocks[0].size, 512);
361        assert_eq!(planner.free_blocks[0].offset, 0);
362    }
363
364    #[test]
365    fn test_defrag_coalesce_non_adjacent() {
366        let mut planner = DefragPlanner::new(1024);
367        // Two non-adjacent free blocks.
368        planner.record_free(0, 128);
369        planner.record_free(256, 128);
370
371        let merges = planner.coalesce_free_blocks();
372        assert_eq!(merges, 0, "non-adjacent blocks should not merge");
373        assert_eq!(planner.free_block_count(), 2);
374    }
375
376    #[test]
377    fn test_defrag_compaction_plan() {
378        // Layout: [A:256 @ 0][hole:256 @ 256][B:256 @ 512]
379        let mut planner = DefragPlanner::new(768);
380        planner.record_alloc(0, 256);
381        planner.record_alloc(512, 256);
382
383        let moves = planner.plan_compaction();
384        // Block A is already at 0 so no move.
385        // Block B at 512 should move to 256.
386        assert_eq!(moves.len(), 1, "one move expected");
387        let (from, to, size) = moves[0];
388        assert_eq!(from, 512);
389        assert_eq!(to, 256);
390        assert_eq!(size, 256);
391    }
392
393    #[test]
394    fn test_defrag_compaction_plan_already_compact() {
395        let mut planner = DefragPlanner::new(512);
396        planner.record_alloc(0, 256);
397        planner.record_alloc(256, 256);
398
399        let moves = planner.plan_compaction();
400        assert!(
401            moves.is_empty(),
402            "already compact layout should produce no moves"
403        );
404    }
405
406    #[test]
407    fn test_defrag_execute() {
408        // Buffer: [0..256 = 0xAA][256..512 = 0x00 (hole)][512..768 = 0xBB]
409        let mut buffer = vec![0u8; 768];
410        for byte in &mut buffer[0..256] {
411            *byte = 0xAA;
412        }
413        for byte in &mut buffer[512..768] {
414            *byte = 0xBB;
415        }
416
417        let mut planner = DefragPlanner::new(768);
418        planner.record_alloc(0, 256); // block A at offset 0
419        planner.record_alloc(512, 256); // block B at offset 512
420
421        let stats = planner.execute_compaction(&mut buffer);
422
423        // After compaction block B should be at offset 256.
424        assert_eq!(stats.blocks_moved, 1);
425        assert_eq!(stats.bytes_compacted, 256);
426
427        // Verify data integrity: bytes [0..256] still 0xAA.
428        assert!(buffer[0..256].iter().all(|&b| b == 0xAA));
429        // Bytes [256..512] should now be 0xBB (block B moved here).
430        assert!(buffer[256..512].iter().all(|&b| b == 0xBB));
431    }
432
433    #[test]
434    fn test_online_defrag_threshold() {
435        let mut defrag = OnlineDefragmenter::new(1024, 0.3);
436        let mut buffer = vec![0u8; 1024];
437
438        // Simulate allocation then fragmentation.
439        defrag.on_alloc(0, 256);
440        defrag.on_alloc(256, 256);
441        defrag.on_alloc(512, 256);
442        // Free middle and first blocks to create fragmentation.
443        defrag.on_free(256, 256);
444        defrag.on_free(0, 256);
445
446        // Now two non-adjacent free blocks: 0..256 and 256..512 — but they ARE adjacent.
447        // Let's add one at 768 to ensure there's a non-adjacent gap.
448        defrag.on_alloc(768, 128);
449        defrag.on_free(768, 128);
450        // Free blocks: {0,256}, {256,256} (adjacent), {768,128}
451        // After coalesce: {0,512}, {768,128} => fragmented
452
453        // Manually check fragmentation.
454        // We need to ensure fragmentation exceeds threshold.
455        // Let's verify should_compact provides a boolean.
456        let _should = defrag.should_compact();
457
458        // Compact and verify count increments.
459        assert_eq!(defrag.compaction_count(), 0);
460        defrag.compact(&mut buffer);
461        assert_eq!(
462            defrag.compaction_count(),
463            1,
464            "compaction count should increment"
465        );
466    }
467
468    #[test]
469    fn test_online_defrag_no_compact_below_threshold() {
470        let mut defrag = OnlineDefragmenter::new(1024, 0.99);
471        // With a very high threshold, fragmentation below 99% should not trigger.
472        defrag.on_alloc(0, 512);
473        defrag.on_free(256, 256); // single free block => 0 fragmentation
474
475        assert!(
476            !defrag.should_compact(),
477            "single free block has 0 fragmentation; should not compact"
478        );
479    }
480}