@@ -11,8 +11,8 @@ namespace MemoryManager.Core
1111 /// <inheritdoc />
1212 /// <summary>
1313 /// A Linear/Bump allocator designed for large or unpredictable blobs of memory.
14- /// Allocations are extremely fast, but memory is only reclaimed when the entire
15- /// manager is compacted/cleared .
14+ /// Allocations are extremely fast, leveraging a dense flat array for metadata tracking.
15+ /// Memory is physically reclaimed and consolidated during explicit compaction .
1616 /// </summary>
1717 /// <seealso cref="IMemoryLane" />
1818 public sealed class BlobManager : IMemoryLane
@@ -43,9 +43,9 @@ public sealed class BlobManager : IMemoryLane
4343 private int _nextFreeOffset ;
4444
4545 /// <summary>
46- /// Maps the handle IDs to their actual blob metadata.
46+ /// Dense flat array mapping handles directly to their metadata via (StartingId - Id)
4747 /// </summary>
48- private readonly Dictionary < int , BlobEntry > _entries = new ( ) ;
48+ private BlobEntry [ ] _entries = new BlobEntry [ 128 ] ;
4949
5050#if DEBUG
5151 /// <summary>
@@ -55,16 +55,12 @@ public sealed class BlobManager : IMemoryLane
5555#endif
5656
5757 /// <inheritdoc />
58- public int EntryCount => _entries . Count ;
58+ public int EntryCount { get ; private set ; }
5959
6060 /// <inheritdoc />
6161 public event Action < string > ? OnCompaction ;
6262
6363 /// <inheritdoc />
64- /// <summary>#
65- /// Defined by interface, but not utilized in a fixed-size bump allocator
66- /// Occurs when [on allocation extension].
67- /// </summary>
6864 public event Action < string , int , int > ? OnAllocationExtension ;
6965
7066 /// <summary>
@@ -77,18 +73,12 @@ public BlobManager(nint buffer, int capacity)
7773 }
7874
7975 /// <inheritdoc />
80- /// <summary>
81- /// Checks if the memory lane can allocate a block of the specified size.
82- /// </summary>
8376 public bool CanAllocate ( int size )
8477 {
8578 return _nextFreeOffset + size <= _capacity ;
8679 }
8780
8881 /// <inheritdoc />
89- /// <summary>
90- /// Allocates a contiguous block of memory by bumping the offset forward.
91- /// </summary>
9282 public MemoryHandle Allocate (
9383 int size ,
9484 AllocationPriority priority = AllocationPriority . Normal ,
@@ -100,19 +90,20 @@ public MemoryHandle Allocate(
10090 throw new OutOfMemoryException (
10191 $ "BlobManager: Out of memory. Requested { size } bytes, but only { FreeSpace ( ) } available.") ;
10292
103- // Prevent negative ID underflow wrapping into positive integers
10493 if ( _nextId == int . MinValue )
105- throw new OutOfMemoryException ( "BlobManager: ID exhaustion. Cannot allocate more IDs without violating negative ID convention ." ) ;
94+ throw new OutOfMemoryException ( "BlobManager: ID exhaustion. Cannot allocate more IDs." ) ;
10695
10796 var id = _nextId -- ;
108- var allocatedOffset = _nextFreeOffset ;
97+ int index = StartingId - id ; // Maps sequential negative IDs cleanly to 0, 1, 2...
98+
99+ EnsureCapacity ( index ) ;
109100
110101 byte version = 1 ;
111102
112- _entries [ id ] = new BlobEntry
103+ _entries [ index ] = new BlobEntry
113104 {
114105 Id = id ,
115- Offset = allocatedOffset ,
106+ Offset = _nextFreeOffset ,
116107 Size = size ,
117108 AllocationFrame = currentFrame ,
118109 Version = version
@@ -127,29 +118,30 @@ public MemoryHandle Allocate(
127118
128119 // Bump the allocator forward
129120 _nextFreeOffset += size ;
121+ EntryCount ++ ;
130122
131123 return new MemoryHandle ( id , version , this ) ;
132124 }
133125
134126 /// <inheritdoc />
135- /// <summary>
136- /// Invalidates the handle.
137- /// Note: As a bump allocator, this does NOT immediately reclaim the physical memory bytes.
138- /// </summary>
139127 public void Free ( MemoryHandle handle )
140128 {
141- if ( ! _entries . Remove ( handle . Id ) )
129+ int index = StartingId - handle . Id ;
130+
131+ if ( index < 0 || index >= _entries . Length || _entries [ index ] . Size == 0 )
142132 throw new InvalidOperationException ( $ "BlobManager: Invalid or double-freed handle { handle . Id } ") ;
143133
134+ _entries [ index ] = default ;
135+ EntryCount -- ;
136+
144137#if DEBUG
145138 _debugNames . Remove ( handle . Id ) ;
146139#endif
147140 }
148141
149142 /// <summary>
150- /// Frees the many .
143+ /// Frees multiple handles sequentially .
151144 /// </summary>
152- /// <param name="handles">The handles.</param>
153145 public void FreeMany ( IEnumerable < MemoryHandle > handles )
154146 {
155147 foreach ( var handle in handles )
@@ -161,24 +153,36 @@ public void FreeMany(IEnumerable<MemoryHandle> handles)
161153 /// <inheritdoc />
162154 public nint Resolve ( MemoryHandle handle )
163155 {
164- if ( ! _entries . TryGetValue ( handle . Id , out var entry ) )
156+ int index = StartingId - handle . Id ;
157+
158+ if ( index < 0 || index >= _entries . Length || _entries [ index ] . Size == 0 )
165159 throw new InvalidOperationException ( $ "BlobManager: Invalid handle { handle . Id } ") ;
166160
161+ ref readonly var entry = ref _entries [ index ] ;
162+
167163 if ( entry . Version != handle . Version )
168164 throw new AccessViolationException ( $ "Blob Zombie: ID { handle . Id } version mismatch.") ;
169165
170166 return _buffer + entry . Offset ;
171167 }
172168
173169 /// <inheritdoc />
174- public bool HasHandle ( MemoryHandle handle ) => _entries . ContainsKey ( handle . Id ) ;
170+ public bool HasHandle ( MemoryHandle handle )
171+ {
172+ int index = StartingId - handle . Id ;
173+ return index >= 0 && index < _entries . Length && _entries [ index ] . Size > 0 ;
174+ }
175175
176176 /// <inheritdoc />
177177 public AllocationEntry GetEntry ( MemoryHandle handle )
178178 {
179- if ( ! _entries . TryGetValue ( handle . Id , out var blob ) )
179+ int index = StartingId - handle . Id ;
180+
181+ if ( index < 0 || index >= _entries . Length || _entries [ index ] . Size == 0 )
180182 throw new InvalidOperationException ( $ "BlobManager: Invalid handle { handle . Id } ") ;
181183
184+ ref readonly var blob = ref _entries [ index ] ;
185+
182186 return new AllocationEntry
183187 {
184188 HandleId = blob . Id ,
@@ -197,41 +201,38 @@ public AllocationEntry GetEntry(MemoryHandle handle)
197201 /// <inheritdoc />
198202 public int GetAllocationSize ( MemoryHandle handle )
199203 {
200- if ( ! _entries . TryGetValue ( handle . Id , out var blob ) )
204+ int index = StartingId - handle . Id ;
205+
206+ if ( index < 0 || index >= _entries . Length || _entries [ index ] . Size == 0 )
201207 throw new InvalidOperationException ( $ "BlobManager: Invalid handle { handle . Id } ") ;
202208
203- return blob . Size ;
209+ return _entries [ index ] . Size ;
204210 }
205211
206212 /// <inheritdoc />
207- /// <summary>
208- /// WARNING: For a linear allocator, compaction means a total reset.
209- /// This wipes all entries and resets the offset to 0.
210- /// </summary>
211213 public void Compact ( )
212214 {
213- if ( _entries . Count == 0 ) return ;
215+ if ( EntryCount == 0 ) return ;
214216
215- // 1. Rent a temporary buffer for zero-allocation sorting
216217 var pool = System . Buffers . ArrayPool < BlobEntry > . Shared ;
217- var scratch = pool . Rent ( _entries . Count ) ;
218+ var scratch = pool . Rent ( EntryCount ) ;
218219
219220 try
220221 {
221222 var validCount = 0 ;
222- foreach ( var entry in _entries . Values )
223+ for ( var i = 0 ; i < _entries . Length ; i ++ )
223224 {
224- scratch [ validCount ++ ] = entry ;
225+ if ( _entries [ i ] . Size > 0 )
226+ {
227+ scratch [ validCount ++ ] = _entries [ i ] ;
228+ }
225229 }
226230
227231 var validSpan = scratch . AsSpan ( 0 , validCount ) ;
228-
229- // Sort natively using a struct to avoid delegate allocation
230232 validSpan . Sort ( new BlobOffsetComparer ( ) ) ;
231233
232234 var currentOffset = 0 ;
233235
234- // 2. Slide each entry as far left as possible
235236 for ( var i = 0 ; i < validSpan . Length ; i ++ )
236237 {
237238 ref readonly var entry = ref validSpan [ i ] ;
@@ -240,7 +241,6 @@ public void Compact()
240241 {
241242 unsafe
242243 {
243- // System.Buffer.MemoryCopy safely handles overlapping memory
244244 System . Buffer . MemoryCopy (
245245 ( void * ) ( _buffer + entry . Offset ) ,
246246 ( void * ) ( _buffer + currentOffset ) ,
@@ -249,15 +249,13 @@ public void Compact()
249249 }
250250 }
251251
252- // 3. Update the entry's metadata with its new physical address
253- var updatedEntry = entry ;
254- updatedEntry . Offset = currentOffset ;
255- _entries [ entry . Id ] = updatedEntry ;
252+ // Update the offset inside our primary dense track tracking array slot directly
253+ int index = StartingId - entry . Id ;
254+ _entries [ index ] . Offset = currentOffset ;
256255
257256 currentOffset += entry . Size ;
258257 }
259258
260- // 4. Update the global allocator offset
261259 _nextFreeOffset = currentOffset ;
262260 }
263261 finally
@@ -271,30 +269,29 @@ public void Compact()
271269 /// <inheritdoc />
272270 public double UsagePercentage ( )
273271 {
274- return _nextFreeOffset / ( double ) _capacity * 100.0 ;
272+ // Normalizes return behavior with FastLane/SlowLane metrics (returns scale 0.0 - 1.0 instead of 0.0 - 100.0)
273+ return _capacity == 0 ? 0.0 : _nextFreeOffset / ( double ) _capacity ;
275274 }
276275
277276 /// <inheritdoc />
278277 public int FreeSpace ( ) => _capacity - _nextFreeOffset ;
279278
280279 /// <inheritdoc />
281- /// <summary>
282- /// Calculates the percentage of "dead" space behind the bump pointer
283- /// that can be reclaimed via Compaction.
284- /// </summary>
285280 public int EstimateFragmentation ( )
286281 {
287282 var allocatedBytes = _nextFreeOffset ;
288283 if ( allocatedBytes == 0 ) return 0 ;
289284
290285 var livingBytes = 0 ;
291- foreach ( var blob in _entries . Values )
286+ for ( var i = 0 ; i < _entries . Length ; i ++ )
292287 {
293- livingBytes += blob . Size ;
288+ if ( _entries [ i ] . Size > 0 )
289+ {
290+ livingBytes += _entries [ i ] . Size ;
291+ }
294292 }
295293
296294 var wastedBytes = allocatedBytes - livingBytes ;
297-
298295 return ( int ) ( ( double ) wastedBytes / allocatedBytes * 100 ) ;
299296 }
300297
@@ -304,29 +301,27 @@ public int EstimateFragmentation()
304301 /// <inheritdoc />
305302 public IEnumerable < MemoryHandle > GetHandles ( )
306303 {
307- foreach ( var entry in _entries . Values )
304+ for ( var i = 0 ; i < _entries . Length ; i ++ )
308305 {
309- // Return the correct, versioned handle
310- yield return new MemoryHandle ( entry . Id , entry . Version , this ) ;
306+ if ( _entries [ i ] . Size > 0 )
307+ {
308+ yield return new MemoryHandle ( _entries [ i ] . Id , _entries [ i ] . Version , this ) ;
309+ }
311310 }
312311 }
313312
314313 /// <inheritdoc />
315314 public string DebugDump ( )
316315 {
317- return $ "BlobManager Dump\n Allocations: { _entries . Count } \n Used: { _nextFreeOffset } /{ _capacity } bytes";
316+ return $ "BlobManager Dump\n Allocations: { EntryCount } \n Used: { _nextFreeOffset } /{ _capacity } bytes";
318317 }
319318
320319 /// <inheritdoc />
321- /// <summary>
322- /// Generates a visual string representation of the BlobManager's memory layout.
323- /// █ = Living Data, - = Dead Gap (Wasted), ░ = Untouched Capacity
324- /// </summary>
325320 public string DebugVisualMap ( )
326321 {
327322 if ( _capacity == 0 ) return "[]" ;
328323
329- const int mapResolution = 80 ; // Width of the console map
324+ const int mapResolution = 80 ;
330325 var map = new char [ mapResolution ] ;
331326 var bytesPerChar = ( double ) _capacity / mapResolution ;
332327
@@ -335,19 +330,16 @@ public string DebugVisualMap()
335330 var startByte = i * bytesPerChar ;
336331 var endByte = ( i + 1 ) * bytesPerChar ;
337332
338- // If this bucket is completely past the bump pointer, it's untouched.
339333 if ( startByte >= _nextFreeOffset )
340334 {
341335 map [ i ] = '░' ;
342336 continue ;
343337 }
344338
345- // Otherwise, check if any living blob intersects this bucket
346339 var isLiving = false ;
347- foreach ( var blob in _entries . Values )
340+ for ( var j = 0 ; j < _entries . Length ; j ++ )
348341 {
349- // Intersection math: Blob starts before bucket ends AND Blob ends after bucket starts
350- if ( blob . Offset < endByte && blob . Offset + blob . Size > startByte )
342+ if ( _entries [ j ] . Size > 0 && _entries [ j ] . Offset < endByte && _entries [ j ] . Offset + _entries [ j ] . Size > startByte )
351343 {
352344 isLiving = true ;
353345 break ;
@@ -363,14 +355,27 @@ public string DebugVisualMap()
363355 /// <inheritdoc />
364356 public string DebugRedirections ( ) => "[BlobRedirects not applicable]" ;
365357
366- /// <inheritdoc />
358+ /// <summary>
359+ /// Resizes the tracking entry metadata array geometric pool size.
360+ /// </summary>
361+ private void EnsureCapacity ( int requiredIndex )
362+ {
363+ if ( requiredIndex >= _entries . Length )
364+ {
365+ var oldSize = _entries . Length ;
366+ var newSize = oldSize * 2 ;
367+ if ( newSize <= requiredIndex ) newSize = requiredIndex + 1 ;
368+
369+ Array . Resize ( ref _entries , newSize ) ;
370+ OnAllocationExtension ? . Invoke ( nameof ( BlobManager ) , oldSize , newSize ) ;
371+ }
372+ }
373+
367374 /// <summary>
368375 /// Zero-allocation comparer for sorting blobs by offset.
369376 /// </summary>
370- /// <seealso cref="System.Collections.Generic.IComparer<MemoryManager.Core.BlobEntry>" />
371377 private struct BlobOffsetComparer : IComparer < BlobEntry >
372378 {
373- /// <inheritdoc />
374379 public int Compare ( BlobEntry x , BlobEntry y )
375380 {
376381 return x . Offset . CompareTo ( y . Offset ) ;
0 commit comments