Skip to content

Commit 90b4a7c

Browse files
author
LoneWandererProductions
committed
Fix up the Slowlane for good.
1 parent fb9cd77 commit 90b4a7c

8 files changed

Lines changed: 415 additions & 86 deletions

File tree

ExtendedSystemObjects/CategorizedDictionary.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ public string GetCategory(TK key)
277277
/// <returns>True if success.</returns>
278278
public bool SetCategory(TK key, string newCategory)
279279
{
280-
// Fix: Normalize input category to ensure consistency with Add()
280+
// Normalize input category to ensure consistency with Add()
281281
newCategory = NormalizeCategory(newCategory);
282282

283283
_lock.EnterWriteLock();
@@ -321,7 +321,7 @@ public IEnumerable<string> GetCategories()
321321
_lock.EnterReadLock();
322322
try
323323
{
324-
// Fix: Must snapshot keys inside the lock to allow safe iteration outside
324+
// Must snapshot keys inside the lock to allow safe iteration outside
325325
return new List<string>(_categories.Keys);
326326
}
327327
finally { _lock.ExitReadLock(); }
@@ -340,7 +340,7 @@ public IEnumerable<TK> GetKeys(string category)
340340
{
341341
if (_categories.TryGetValue(category, out var set))
342342
{
343-
// Fix: Must snapshot the HashSet to allow safe iteration outside
343+
// Must snapshot the HashSet to allow safe iteration outside
344344
return new List<TK>(set);
345345
}
346346

@@ -360,7 +360,7 @@ public IEnumerable<TK> GetKeys()
360360
_lock.EnterReadLock();
361361
try
362362
{
363-
// Fix: Must snapshot keys inside the lock
363+
// Must snapshot keys inside the lock
364364
return new List<TK>(_data.Keys);
365365
}
366366
finally { _lock.ExitReadLock(); }

ExtendedSystemObjects/UnmanagedIntList.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ public void InsertAt(int index, int value, int count = 1)
392392
public Span<int> AsSpan()
393393
{
394394
EnsureNotDisposed();
395-
// Fix: Use 'Length' instead of 'Capacity'
395+
// Use 'Length' instead of 'Capacity'
396396
// to prevent access to uninitialized memory.
397397
return new Span<int>(_ptr, Length);
398398
}

MemoryManager.Lanes/SlowLane.cs

Lines changed: 148 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,10 @@
33
* PROJECT: MemoryArenaPrototype.Lane
44
* FILE: SlowLane.cs
55
* PURPOSE: Memory store for long lived data and stuff we could not hold into the fast lane.
6-
* Ids for Allocations is always negative here.
6+
* Ids for Allocations is always negative here.
77
* PROGRAMMER: Peter Geinitz (Wayfarer)
88
*/
99

10-
// TODO Compaction
11-
// we should configure two options good enough or full compaction
12-
// via the enum CompactionStyle.
13-
// Good enough would stop as soon as a big enough gap is opened, while full would always compact everything to the start of the buffer.
14-
1510
// ReSharper disable EventNeverSubscribedTo.Global
1611

1712
using ExtendedSystemObjects;
@@ -83,6 +78,11 @@ public sealed class SlowLane : IMemoryLane, IDisposable
8378
/// </summary>
8479
private readonly int _blobThreshold = 256;
8580

81+
/// <summary>
82+
/// The physical byte capacity assigned to the BlobManager sub-allocator.
83+
/// </summary>
84+
private readonly int _blobCapacity;
85+
8686
/// <summary>
8787
/// The specialized manager for tiny/unpredictable allocations.
8888
/// </summary>
@@ -103,6 +103,11 @@ public sealed class SlowLane : IMemoryLane, IDisposable
103103
/// </summary>
104104
private readonly AllocationStrategy _searchStrategy;
105105

106+
/// <summary>
107+
/// Gets or sets the compaction strategy used when defragmenting the lane buffer.
108+
/// </summary>
109+
public CompactionStyle CompactionStyle { get; set; }
110+
106111
/// <summary>
107112
/// Initializes a new instance of the <see cref="SlowLane" /> class.
108113
/// </summary>
@@ -111,14 +116,21 @@ public sealed class SlowLane : IMemoryLane, IDisposable
111116
/// <param name="blobThreshold">The BLOB threshold.</param>
112117
/// <param name="maxEntries">The maximum entries.</param>
113118
/// <param name="slowLaneFreeListStrategy">The slow lane free list strategy.</param>
114-
public unsafe SlowLane(int capacity, double blobCapacityFraction = 0.20, int blobThreshold = 256,
115-
int maxEntries = 1024, AllocationStrategy slowLaneFreeListStrategy = default)
119+
/// <param name="compactionStyle">The compaction style policy (Full or GoodEnough).</param>
120+
public unsafe SlowLane(
121+
int capacity,
122+
double blobCapacityFraction = 0.20,
123+
int blobThreshold = 256,
124+
int maxEntries = 1024,
125+
AllocationStrategy slowLaneFreeListStrategy = default,
126+
CompactionStyle compactionStyle = CompactionStyle.Full)
116127
{
117128
Capacity = capacity;
118129
_blobThreshold = blobThreshold;
119130

120-
// CAPTURE STRATEGY: Persist the configuration choice for all future allocation passes
131+
// CAPTURE STRATEGIES: Persist configured choices
121132
_searchStrategy = slowLaneFreeListStrategy;
133+
CompactionStyle = compactionStyle;
122134

123135
Buffer = Marshal.AllocHGlobal(capacity);
124136
_entries = new AllocationEntry[maxEntries];
@@ -128,12 +140,12 @@ public unsafe SlowLane(int capacity, double blobCapacityFraction = 0.20, int blo
128140
_versions = (uint*)NativeMemory.AllocZeroed((nuint)_versionsCapacity, sizeof(uint));
129141

130142
// Use the fraction provided by the config/constructor
131-
var blobCapacity = (int)(capacity * blobCapacityFraction);
143+
_blobCapacity = (int)(capacity * blobCapacityFraction);
132144

133-
_freeBlocks[0] = new FreeBlock { Offset = blobCapacity, Size = Capacity - blobCapacity };
145+
_freeBlocks[0] = new FreeBlock { Offset = _blobCapacity, Size = Capacity - _blobCapacity };
134146
_freeBlockCount = 1;
135147

136-
_blobManager = new BlobManager(Buffer, blobCapacity);
148+
_blobManager = new BlobManager(Buffer, _blobCapacity);
137149
}
138150

139151
/// <summary>
@@ -206,11 +218,17 @@ public unsafe MemoryHandle Allocate(
206218

207219
// Calculate tracking footprint dimension rules including canary bytes
208220
var physicalSizeNeeded = MemoryCanary.GetPhysicalSize(size);
209-
var offset = MemoryLaneUtils.FindFreeSpot(physicalSizeNeeded, ref _freeBlocks, ref _freeBlockCount,
210-
_searchStrategy);
221+
var offset = MemoryLaneUtils.FindFreeSpot(physicalSizeNeeded, ref _freeBlocks, ref _freeBlockCount, _searchStrategy);
211222

223+
// AUTO-HEALING: If fragmented, trigger policy-based compaction to open up a contiguous gap
212224
if (offset == -1)
213-
throw new OutOfMemoryException("SlowLane: Cannot allocate - No contiguous block large enough.");
225+
{
226+
Compact(CompactionStyle, size);
227+
offset = MemoryLaneUtils.FindFreeSpot(physicalSizeNeeded, ref _freeBlocks, ref _freeBlockCount, _searchStrategy);
228+
229+
if (offset == -1)
230+
throw new OutOfMemoryException("SlowLane: Cannot allocate - No contiguous block large enough after compaction.");
231+
}
214232

215233
// Shift user coordinates forward safely past the pre-canary buffer space
216234
var userOffset = MemoryCanary.GetUserOffset(offset);
@@ -405,14 +423,21 @@ public unsafe void FreeMany(MemoryHandle[] handles)
405423
}
406424

407425
/// <inheritdoc />
408-
public unsafe void Compact()
426+
public void Compact()
409427
{
410-
if (_entries == null || _handleIndex.Count == 0) return;
428+
Compact(CompactionStyle, 0);
429+
}
411430

412-
var newBuffer = Marshal.AllocHGlobal(Capacity);
413-
var offset = 0;
431+
/// <summary>
432+
/// Compacts the unmanaged lane buffer using the specified policy and target size requirement.
433+
/// </summary>
434+
/// <param name="style">The compaction style (Full or GoodEnough).</param>
435+
/// <param name="requiredSize">Target size required if GoodEnough style is active.</param>
436+
public unsafe void Compact(CompactionStyle style, int requiredSize = 0)
437+
{
438+
if (_entries == null || _handleIndex.Count == 0) return;
414439

415-
// 1. Extract only the living, valid entries using our dictionary
440+
// 1. Extract non-stub living entries
416441
var validEntries = new List<AllocationEntry>(_handleIndex.Count);
417442
foreach (var index in _handleIndex.Values)
418443
{
@@ -423,67 +448,124 @@ public unsafe void Compact()
423448
}
424449
}
425450

426-
// 2. Sort them by their physical Offset in the buffer
427-
validEntries.Sort((a, b) => a.Offset.CompareTo(b.Offset));
451+
if (validEntries.Count == 0)
452+
{
453+
_freeBlocks[0] = new FreeBlock
454+
{
455+
Offset = _blobCapacity,
456+
Size = Capacity - _blobCapacity
457+
};
458+
_freeBlockCount = 1;
459+
_freeSlots.Clear();
460+
EntryCount = 0;
461+
return;
462+
}
428463

429-
var writeIndex = 0;
430-
var newHandleIndex = new Dictionary<int, int>(validEntries.Count);
464+
// 2. Sort by current physical offset ascending
465+
validEntries.Sort((a, b) => MemoryCanary.GetPhysicalOffset(a.Offset).CompareTo(MemoryCanary.GetPhysicalOffset(b.Offset)));
431466

432-
// 3. Copy them sequentially to the new buffer
433-
foreach (var entry in validEntries)
467+
var currentOffset = _blobCapacity;
468+
var physicalTargetNeeded = requiredSize > 0 ? MemoryCanary.GetPhysicalSize(requiredSize) : 0;
469+
var isGoodEnough = style == CompactionStyle.GoodEnough;
470+
471+
// 3. Perform surgical in-place sliding
472+
for (var i = 0; i < validEntries.Count; i++)
434473
{
435-
var currentEntry = entry; // Make a local copy to modify
474+
var entry = validEntries[i];
475+
var srcPhysicalOffset = MemoryCanary.GetPhysicalOffset(entry.Offset);
476+
var physicalSize = MemoryCanary.GetPhysicalSize(entry.Size);
436477

437-
// Extract base track coordinates for the total block footprint
438-
var srcPhysicalOffset = MemoryCanary.GetPhysicalOffset(currentEntry.Offset);
439-
var physicalSize = MemoryCanary.GetPhysicalSize(currentEntry.Size);
478+
if (srcPhysicalOffset != currentOffset)
479+
{
480+
// Slide block in-place (System.Buffer.MemoryCopy handles overlapping regions safely when copying downward)
481+
System.Buffer.MemoryCopy(
482+
(void*)(Buffer + srcPhysicalOffset),
483+
(void*)(Buffer + currentOffset),
484+
physicalSize,
485+
physicalSize);
440486

441-
// Slide the complete unmanaged layout block (Canary + User Data + Canary)
442-
System.Buffer.MemoryCopy(
443-
(void*)(Buffer + srcPhysicalOffset),
444-
(void*)(newBuffer + offset),
445-
physicalSize,
446-
physicalSize);
487+
entry.Offset = MemoryCanary.GetUserOffset(currentOffset);
447488

448-
// Realign tracked metadata pointers to target the new user offset location
449-
currentEntry.Offset = MemoryCanary.GetUserOffset(offset);
450-
offset += physicalSize;
489+
if (_handleIndex.TryGetValue(entry.HandleId, out var slotIndex))
490+
{
491+
_entries[slotIndex] = entry;
492+
}
451493

452-
EnsureEntryCapacity(writeIndex);
453-
_entries[writeIndex] = currentEntry;
454-
newHandleIndex[currentEntry.HandleId] = writeIndex;
494+
validEntries[i] = entry;
495+
}
455496

456-
writeIndex++;
457-
}
497+
currentOffset += physicalSize;
458498

459-
// 4. Clear all remaining slots
460-
for (var i = writeIndex; i < _entries.Length; i++)
461-
{
462-
_entries[i] = default;
499+
// 4. Evaluate "GoodEnough" stopping condition
500+
if (isGoodEnough && physicalTargetNeeded > 0)
501+
{
502+
var nextEntryOffset = i + 1 < validEntries.Count
503+
? MemoryCanary.GetPhysicalOffset(validEntries[i + 1].Offset)
504+
: Capacity;
505+
506+
var gap = nextEntryOffset - currentOffset;
507+
if (gap >= physicalTargetNeeded)
508+
{
509+
// Target gap created! Stop shifting remaining blocks to save CPU cycles.
510+
break;
511+
}
512+
}
463513
}
464514

465-
// 5. Update the internal state
466-
Marshal.FreeHGlobal(Buffer);
467-
Buffer = newBuffer;
515+
// 5. Rebuild free-list blocks accurately
516+
RebuildFreeBlocks(validEntries);
468517

469-
_handleIndex.Clear();
470-
foreach (var kv in newHandleIndex)
518+
OnCompaction?.Invoke(nameof(SlowLane));
519+
}
520+
521+
/// <summary>
522+
/// Rebuilds the free block list based on active allocation positions.
523+
/// </summary>
524+
private void RebuildFreeBlocks(List<AllocationEntry> validEntries)
525+
{
526+
validEntries.Sort((a, b) => MemoryCanary.GetPhysicalOffset(a.Offset).CompareTo(MemoryCanary.GetPhysicalOffset(b.Offset)));
527+
528+
_freeBlockCount = 0;
529+
var cursor = _blobCapacity;
530+
531+
foreach (var entry in validEntries)
471532
{
472-
_handleIndex[kv.Key] = kv.Value;
473-
}
533+
var physOffset = MemoryCanary.GetPhysicalOffset(entry.Offset);
534+
var physSize = MemoryCanary.GetPhysicalSize(entry.Size);
535+
536+
if (physOffset > cursor)
537+
{
538+
EnsureFreeBlockArrayCapacity(_freeBlockCount + 1);
539+
_freeBlocks[_freeBlockCount++] = new FreeBlock
540+
{
541+
Offset = cursor,
542+
Size = physOffset - cursor
543+
};
544+
}
474545

475-
EntryCount = writeIndex;
476-
_freeSlots.Clear(); // No more holes in the array!
546+
cursor = physOffset + physSize;
547+
}
477548

478-
// 6. Reset the Free-List!
479-
_freeBlocks[0] = new FreeBlock
549+
if (cursor < Capacity)
480550
{
481-
Offset = offset,
482-
Size = Capacity - offset
483-
};
484-
_freeBlockCount = 1;
551+
EnsureFreeBlockArrayCapacity(_freeBlockCount + 1);
552+
_freeBlocks[_freeBlockCount++] = new FreeBlock
553+
{
554+
Offset = cursor,
555+
Size = Capacity - cursor
556+
};
557+
}
558+
}
485559

486-
OnCompaction?.Invoke(nameof(SlowLane));
560+
/// <summary>
561+
/// Ensures free blocks array has capacity for required count.
562+
/// </summary>
563+
private void EnsureFreeBlockArrayCapacity(int requiredCount)
564+
{
565+
if (_freeBlocks.Length < requiredCount)
566+
{
567+
Array.Resize(ref _freeBlocks, Math.Max(_freeBlocks.Length * 2, requiredCount));
568+
}
487569
}
488570

489571
/// <inheritdoc />
@@ -573,7 +655,7 @@ private int GetUsed()
573655
/// <inheritdoc />
574656
public int FreeSpace()
575657
{
576-
// FIX: Passes the physical tracking source lists to clear type signature compilation errors
658+
// Passes the physical tracking source lists to clear type signature compilation errors
577659
var mainFreeSpace = MemoryLaneUtils.CalculateFreeSpace(_freeBlocks, _freeBlockCount);
578660
var blobFreeSpace = _blobManager?.FreeSpace() ?? 0;
579661

@@ -628,7 +710,7 @@ public int StubCount()
628710
/// <inheritdoc />
629711
public int EstimateFragmentation()
630712
{
631-
// FIX: Targets the true unmanaged free blocks tracking array for precise fragmentation readout
713+
// Targets the true unmanaged free blocks tracking array for precise fragmentation readout
632714
return MemoryLaneUtils.EstimateFragmentation(_freeBlocks, _freeBlockCount);
633715
}
634716

MemoryManager.Tests/MemoryArenaInlineTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class MemoryArenaInlineTests
1818
/// Memories the arena can allocate and resolve primitive and structs.
1919
/// </summary>
2020
[TestMethod]
21+
[TestCategory("Correctness")]
2122
public void MemoryArena_GenerationalRedirection_WorksCorrectly()
2223
{
2324
var config = new MemoryManagerConfig
@@ -64,6 +65,7 @@ public void MemoryArena_GenerationalRedirection_WorksCorrectly()
6465
/// Memories the arena zombie handle throws exception.
6566
/// </summary>
6667
[TestMethod]
68+
[TestCategory("Correctness")]
6769
public void MemoryArena_ZombieHandle_ThrowsException()
6870
{
6971
var arena = new MemoryArena(new MemoryManagerConfig { FastLaneSize = 1024 });
@@ -91,6 +93,7 @@ public void MemoryArena_ZombieHandle_ThrowsException()
9193
/// Memories the arena can allocate and resolve primitive and structs with extensions.
9294
/// </summary>
9395
[TestMethod]
96+
[TestCategory("Correctness")]
9497
public void MemoryArenaCanAllocateAndResolvePrimitiveAndStructsWithExtensions()
9598
{
9699
var config = new MemoryManagerConfig

MemoryManager.Tests/MemoryArenaTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public void Setup()
4242
/// Allocates the within fast lane threshold allocates in fast lane.
4343
/// </summary>
4444
[TestMethod]
45+
[TestCategory("Correctness")]
4546
public void AllocateWithinFastLaneThresholdAllocatesInFastLane()
4647
{
4748
var arena = new MemoryArena(_config);

0 commit comments

Comments
 (0)