Skip to content

Commit a1d42f0

Browse files
author
LoneWandererProductions
committed
add tests and mark down todos
1 parent 73d71d4 commit a1d42f0

6 files changed

Lines changed: 236 additions & 15 deletions

File tree

MemoryManager.Core/AllocatorStrategy.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
namespace MemoryManager.Core
1010
{
11+
/// <summary>
12+
/// Enum for possible allocation strategies. This is used to determine which lane to use for a given allocation request, and can be configured globally or per-allocation.
13+
/// </summary>
1114
public enum AllocatorStrategy
1215
{
1316
/// <summary>
@@ -21,5 +24,11 @@ public enum AllocatorStrategy
2124
/// Absolute maximum speed, requires frequent compactions
2225
/// </summary>
2326
LinearBump = 1,
27+
28+
/// <summary>
29+
/// The slab lane,
30+
/// Uses fixed-size bins for small allocations, excellent for uniform patterns, but can lead to fragmentation if misused.
31+
/// </summary>
32+
Slab = 2
2433
}
2534
}

MemoryManager.Core/MemoryManagerConfig.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,5 +297,27 @@ public static MemoryManagerConfig CreateForLowMemory()
297297
SlowLaneFreeListStrategy = AllocationStrategy.BestFit
298298
};
299299
}
300+
301+
/// <summary>
302+
/// NEW PRESET: Creates a configuration tuned for ultra-fast, predictable object pooling.
303+
/// Employs the zero-compaction, fixed-bin segregated SlabLane strategy.
304+
/// </summary>
305+
/// <param name="totalBudget">The total budget.</param>
306+
/// <returns>MemoryManagerConfig instance configured for object pooling.</returns>
307+
public static MemoryManagerConfig CreateForObjectPooling(int totalBudget = 32 * 1024 * 1024)
308+
{
309+
return new MemoryManagerConfig
310+
{
311+
SlowLaneSize = (int)(totalBudget * 0.70),
312+
FastLaneSize = (int)(totalBudget * 0.30),
313+
FastLaneStrategy = AllocatorStrategy.Slab, // Deploy the new Slab architecture!
314+
Threshold = 512, // Bins up to 512 bytes (perfect for game components/entities)
315+
MaxFastLaneAgeFrames = 2400, // Let objects sit longer in hot bins
316+
FastLaneLargeEntryThreshold = 512, // Keep the slots compact and matching our max bin size
317+
EnableAutoCompaction = true,
318+
PolicyCheckInterval = TimeSpan.FromMilliseconds(500),
319+
SlowLaneFreeListStrategy = AllocationStrategy.BestFit // SlowLane catches larger spills via clean best-fit
320+
};
321+
}
300322
}
301323
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: MemoryManager.Tests
4+
* FILE: SlabLaneTests.cs
5+
* PURPOSE: Standalone and integration verification for the segregated SlabLane strategy.
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using MemoryManager.Core;
12+
using MemoryManager.Lanes;
13+
using Microsoft.VisualStudio.TestTools.UnitTesting;
14+
15+
namespace MemoryManager.Tests
16+
{
17+
[TestClass]
18+
public class SlabLaneTests
19+
{
20+
/// <summary>
21+
/// Standalone Test: Verifies that SlabLane rounds up to the correct size class bucket,
22+
/// and allows blazing fast out-of-order deletion and recycling of specific slots.
23+
/// </summary>
24+
[TestMethod]
25+
[TestCategory("SlabLane_Standalone")]
26+
public unsafe void SlabLane_AllocateFreeRecycle_WorksStandalone()
27+
{
28+
// Arrange
29+
const int totalSize = 1024 * 1024; // 1 MB buffer pool
30+
using var slowLane = new SlowLane(totalSize);
31+
32+
// Generate a config where the threshold gives us bins up to 256 bytes (16, 32, 64, 128, 256)
33+
var config = new MemoryManagerConfig { Threshold = 256 };
34+
using var slabLane = new SlabLane(totalSize, slowLane, maxEntries: 100, config: config);
35+
36+
// 1. Allocate a 20-byte item (Should snap to the 32-byte Size Class Bin)
37+
var h1 = slabLane.Allocate(20);
38+
int* ptr1 = (int*)slabLane.Resolve(h1);
39+
*ptr1 = 111;
40+
41+
// 2. Allocate a second 20-byte item (Should take the next slot in the same 32-byte Bin)
42+
var h2 = slabLane.Allocate(20);
43+
int* ptr2 = (int*)slabLane.Resolve(h2);
44+
*ptr2 = 222;
45+
46+
// Assert they are isolated blocks separated by at least the physical slot overhead size
47+
Assert.AreNotEqual((nint)ptr1, (nint)ptr2, "Different allocations must yield unique address locations.");
48+
Assert.AreEqual(111, *ptr1);
49+
Assert.AreEqual(222, *ptr2);
50+
51+
// 3. Free the first handle to return its physical offset back to the 32-byte LIFO stack
52+
slabLane.Free(h1);
53+
54+
// 4. Re-allocate a 20-byte item. It must instantly recycle the slot we just freed!
55+
var h3 = slabLane.Allocate(20);
56+
int* ptr3 = (int*)slabLane.Resolve(h3);
57+
58+
Assert.AreEqual((nint)ptr1, (nint)ptr3, "Slab allocation must instantly recycle the vacant uniform slot address.");
59+
}
60+
61+
/// <summary>
62+
/// Standalone Test: Verifies that if a specific size class bucket runs out of pre-allocated slots,
63+
/// it safely hits an OOM bounds check without crashing or corrupting adjacent memory bins.
64+
/// </summary>
65+
[TestMethod]
66+
[TestCategory("SlabLane_Standalone")]
67+
public void SlabLane_BinExhaustion_ThrowsOutOfMemoryException()
68+
{
69+
// Arrange
70+
// Small lane budget ensures buckets have very tightly bounded slots numbers
71+
const int smallCapacity = 4096;
72+
using var slowLane = new SlowLane(64 * 1024);
73+
74+
var config = new MemoryManagerConfig { Threshold = 64 }; // Generates 16, 32, 64 byte bins (3 bins total)
75+
using var slabLane = new SlabLane(smallCapacity, slowLane, maxEntries: 50, config: config);
76+
77+
// Act & Assert
78+
Assert.ThrowsException<OutOfMemoryException>(() =>
79+
{
80+
// Rapidly flood the 64-byte bucket line until its isolated sub-partition layout breaks
81+
for (int i = 0; i < 100; i++)
82+
{
83+
slabLane.Allocate(60);
84+
}
85+
}, "A single saturated size bin must reject requests cleanly via OutOfMemoryException once exhausted.");
86+
}
87+
88+
/// <summary>
89+
/// Integration Test: Plugs the SlabLane directly into the high-level MemoryArena facade
90+
/// and verifies automated allocation orchestration and internal fragmentation tracking.
91+
/// </summary>
92+
[TestMethod]
93+
[TestCategory("Arena_SlabIntegration")]
94+
public unsafe void Arena_WithSlabLane_OrchestratesAllocationsPerfect()
95+
{
96+
// Arrange
97+
var config = new MemoryManagerConfig
98+
{
99+
FastLaneSize = 512 * 1024,
100+
SlowLaneSize = 2 * 1024 * 1024,
101+
Threshold = 256,
102+
FastLaneStrategy = AllocatorStrategy.Slab // Activate our drop-in engine replacement!
103+
};
104+
105+
var arena = new MemoryArena(config);
106+
107+
// 1. Allocate down into the slab lane via the main unified entry door
108+
var handle = arena.Allocate(45); // Snaps to 64-byte bin inside SlabLane
109+
110+
Assert.IsInstanceOfType(arena.FastLane, typeof(SlabLane), "The active fast lane implementation must match the Slab wrapper config.");
111+
Assert.IsTrue(handle.Id > 0, "Fast lane allocation handles must maintain positive identity numbers.");
112+
113+
// 2. Perform safe unmanaged read/write actions
114+
ref int dataRef = ref arena.Get<int>(handle);
115+
dataRef = 8888;
116+
117+
Assert.AreEqual(8888, arena.Get<int>(handle));
118+
119+
// 3. Verify internal fragmentation reporting (wasted slot slack space analytics)
120+
int fragPercentage = arena.FastLane.EstimateFragmentation();
121+
Assert.IsTrue(fragPercentage > 0, "Slab lanes must detect and accurately report internal slack-space fragmentation properties.");
122+
123+
// Cleanup
124+
arena.Free(handle);
125+
}
126+
127+
/// <summary>
128+
/// Integration Test: Verifies the full cross-lane handshake during eviction lifecycle steps.
129+
/// Data must drop to the SlowLane, release the slab space, and leave a resolving redirection proxy stub behind.
130+
/// </summary>
131+
[TestMethod]
132+
[TestCategory("Arena_SlabIntegration")]
133+
public unsafe void Arena_SlabEviction_CreatesFunctionalRedirectionStub()
134+
{
135+
// Arrange
136+
var config = new MemoryManagerConfig
137+
{
138+
FastLaneSize = 256 * 1024,
139+
SlowLaneSize = 1024 * 1024,
140+
Threshold = 128,
141+
FastLaneStrategy = AllocatorStrategy.Slab
142+
};
143+
144+
var arena = new MemoryArena(config);
145+
146+
// 1. Establish an item in the slab fast lane and write to it
147+
var handle = arena.Allocate(32); // Takes slot in 32-byte bin
148+
int* initialPtr = (int*)arena.Resolve(handle);
149+
*initialPtr = 777;
150+
151+
// 2. Force an explicit eviction command to push the item out to the SlowLane
152+
arena.MoveFastToSlow(handle);
153+
154+
// 3. ARCHITECTURAL BOUNDARY EVALUATIONS
155+
var entry = arena.GetEntry(handle);
156+
Assert.IsTrue(entry.IsStub, "Evicting data must convert the local metadata slot entry into a proxy tracking stub.");
157+
Assert.AreEqual(0, entry.Size, "Local buffer sizes must register as 0 once vacated by eviction processing paths.");
158+
159+
// 4. STUB TRANSPARENCY VERIFICATION: Resolving the original handle must STILL work cleanly!
160+
int* postEvictionPtr = (int*)arena.Resolve(handle);
161+
Assert.AreNotEqual((nint)initialPtr, (nint)postEvictionPtr, "The memory address space coordinates must have shifted downstream.");
162+
Assert.AreEqual(777, *postEvictionPtr, "The payload values must survive across transfer boundaries completely unaltered.");
163+
164+
// 5. RECYCLING VALIDATION: Ensure the original physical slot is immediately available for fresh requests
165+
var freshHandle = arena.Allocate(32);
166+
int* freshPtr = (int*)arena.Resolve(freshHandle);
167+
168+
Assert.AreEqual((nint)initialPtr, (nint)freshPtr, "The vacated fast lane slab slot must instantly accept new allocation targets.");
169+
}
170+
}
171+
}

MemoryManager/MemoryArena.cs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,28 +67,42 @@ public MemoryArena(MemoryManagerConfig config)
6767
_config = config;
6868
Threshold = config.Threshold;
6969

70-
// PASS 1: Pass the anti-fragmentation strategy flag straight into the SlowLane
70+
// PASS 1: Initialize the anti-fragmentation persistent tier
7171
SlowLane = new SlowLane(
7272
config.SlowLaneSize,
7373
config.SlowLaneBlobCapacityFraction,
7474
config.SlowLaneBlobThreshold,
7575
config.MaxEntries,
76-
config.SlowLaneFreeListStrategy); // Added strategy selection
76+
config.SlowLaneFreeListStrategy);
7777

78-
if (config.FastLaneStrategy == AllocatorStrategy.FreeList)
78+
// PASS 2: Factory Selector for the FastLane layout blueprint
79+
switch (config.FastLaneStrategy)
7980
{
80-
// PASS 2: Pass the high-velocity strategy flag into the Free-List FastLane
81-
FastLane = new FastLane(
82-
config.FastLaneSize,
83-
SlowLane,
84-
config.MaxEntries,
85-
config.FastLaneFreeListStrategy); // Added strategy selection
86-
}
87-
else
88-
{
89-
// NOTE: LinearLane is a pure O(1) bump allocator. It does not scan holes,
90-
// so it does not take an AllocationStrategy configuration parameter.
91-
FastLane = new LinearLane(config.FastLaneSize, SlowLane, config.MaxEntries);
81+
case AllocatorStrategy.FreeList:
82+
FastLane = new FastLane(
83+
config.FastLaneSize,
84+
SlowLane,
85+
config.MaxEntries,
86+
config.FastLaneFreeListStrategy);
87+
break;
88+
89+
case AllocatorStrategy.Slab:
90+
// Drop-in our new dynamic segregated storage bucket allocator
91+
FastLane = new SlabLane(
92+
config.FastLaneSize,
93+
SlowLane,
94+
config.MaxEntries,
95+
config);
96+
break;
97+
98+
case AllocatorStrategy.LinearBump:
99+
default:
100+
// Fall back to our ultra-high speed frame-based linear bump allocator
101+
FastLane = new LinearLane(
102+
config.FastLaneSize,
103+
SlowLane,
104+
config.MaxEntries);
105+
break;
92106
}
93107

94108
FastLane.OneWayLane = new OneWayLane(FastLane, SlowLane);

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ To move this system out of the prototyping phase and into a hardened, production
5050
* **Current State:** When compaction triggers, the compactor engine allocates an entirely new unmanaged heap block via `Marshal.AllocHGlobal` and slides all survivors over via memory block copying. For massive heaps (e.g., 512MB+), this causes noticeable block stutters (latency spikes) and temporarily forces a **double memory footprint**.
5151
* **Production Fix:** Implement an **Incremental/Phased Compactor** that only relocates a small chunk of fragmented blocks per frame tick, or enforce strict zero-compaction rules where arenas are completely cleared and cycled out at deterministic boundaries (e.g., scene changes).
5252

53+
### 3. Add bool IsFragmented() to trigger Compact() on threshold
54+
* ** Current State:** The compactor is only triggered when the arena is completely full. This allows fragmentation to grow unchecked until the last possible moment, causing more severe stutters and higher memory usage.
55+
* **Production Fix:** Introduce an `IsFragmented()` method that evaluates the current fragmentation level against a predefined threshold. If the fragmentation exceeds the threshold, trigger the `Compact()` method proactively to maintain optimal memory usage and reduce latency spikes.
56+
57+
-
5358
---
5459

5560
## 🧩 Config Presets & Advanced Usage

Todos.txt

-112 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)