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}