Skip to content

Commit 71df9c5

Browse files
author
LoneWandererProductions
committed
Add concurrentlane
Add Tests Improve readme
1 parent 9344b71 commit 71df9c5

8 files changed

Lines changed: 629 additions & 69 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: MemoryArenaPrototype.Core
4+
* FILE: IMemoryAllocator.cs
5+
* PURPOSE: Defines the interface for memory allocators managing allocations within the MemoryArena.
6+
* Provides methods for allocation, deallocation, access, compaction, and diagnostics.
7+
* PROGRAMMER: Peter Geinitz (Wayfarer)
8+
*/
9+
10+
namespace MemoryManager.Core
11+
{
12+
/// <summary>
13+
/// Interface representing a memory allocator, responsible for managing memory allocations, deallocations, and access within a memory arena.
14+
/// </summary>
15+
public interface IMemoryAllocator
16+
{
17+
/// <summary>
18+
/// Allocates the specified size.
19+
/// </summary>
20+
/// <param name="size">The size.</param>
21+
/// <param name="priority">The priority.</param>
22+
/// <param name="hints">The hints.</param>
23+
/// <param name="debugName">Name of the debug.</param>
24+
/// <param name="currentFrame">The current frame.</param>
25+
/// <returns>A <see cref="MemoryHandle"/> representing the allocated memory.</returns>
26+
MemoryHandle Allocate(int size, AllocationPriority priority = AllocationPriority.Normal, AllocationHints hints = AllocationHints.None, string? debugName = null, int currentFrame = 0);
27+
28+
/// <summary>
29+
/// Frees the specified handle.
30+
/// </summary>
31+
/// <param name="handle">The handle.</param>
32+
void Free(MemoryHandle handle);
33+
34+
/// <summary>
35+
/// Resolves the specified handle.
36+
/// </summary>
37+
/// <param name="handle">The handle.</param>
38+
/// <returns>A <see cref="nint"/> pointing to the allocated memory block.</returns>
39+
nint Resolve(MemoryHandle handle);
40+
41+
/// <summary>
42+
/// Bulks the set.
43+
/// </summary>
44+
/// <typeparam name="T">The type of the elements.</typeparam>
45+
/// <param name="handle">The handle.</param>
46+
/// <param name="source">The source.</param>
47+
void BulkSet<T>(MemoryHandle handle, ReadOnlySpan<T> source) where T : unmanaged;
48+
49+
/// <summary>
50+
/// Gets the entry.
51+
/// </summary>
52+
/// <param name="handle">The handle.</param>
53+
/// <returns>An <see cref="AllocationEntry"/> representing the allocation entry.</returns>
54+
AllocationEntry GetEntry(MemoryHandle handle);
55+
}
56+
}

MemoryManager.Core/MemoryHandle.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ namespace MemoryManager.Core
2121
/// </summary>
2222
public int Id { get; }
2323

24+
/// <summary>
25+
/// Gets the current memory lane.
26+
/// </summary>
27+
/// <value>
28+
/// The lane controlling this memory handle.
29+
/// </value>
30+
public IMemoryLane Lane => _lane;
31+
2432
/// <summary>
2533
/// The version
2634
/// </summary>
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* COPYRIGHT: See COPYING in the top level directory
3+
* PROJECT: MemoryManager.Tests
4+
* FILE: ConcurrentMemoryArenaTests.cs
5+
* PURPOSE: Concurrency, thread-isolation, and cross-thread freeing verification for ConcurrentMemoryArena.
6+
* PROGRAMMER: Peter Geinitz (Wayfarer)
7+
*/
8+
9+
using System.Collections.Concurrent;
10+
using MemoryManager.Core;
11+
12+
namespace MemoryManager.Tests
13+
{
14+
[TestClass]
15+
public class ConcurrentMemoryArenaTests
16+
{
17+
/// <summary>
18+
/// Concurrency Test: Fires up parallel worker tasks simultaneously.
19+
/// Verifies that every thread allocates out of its own isolated lock-free fast lane
20+
/// without block cross-contamination or address collision crashes.
21+
/// </summary>
22+
[TestMethod]
23+
[TestCategory("Arena_Concurrency")]
24+
public void Arena_ParallelAllocation_MaintainsThreadIsolation()
25+
{
26+
// Arrange
27+
var config = new MemoryManagerConfig
28+
{
29+
FastLaneSize = 256 * 1024, // 256 KB per thread lane
30+
SlowLaneSize = 2 * 1024 * 1024, // 2 MB shared backup
31+
Threshold = 128,
32+
FastLaneStrategy = AllocatorStrategy.Slab // Test with our high-speed Slab strategy
33+
};
34+
35+
var arena = new ConcurrentMemoryArena(config);
36+
const int threadCount = 8;
37+
const int allocationsPerThread = 100;
38+
39+
// Thread-safe collection to aggregate all generated handles across parallel tasks
40+
var allHandles = new ConcurrentBag<MemoryHandle>();
41+
var tasks = new List<Task>();
42+
43+
// Act
44+
for (int t = 0; t < threadCount; t++)
45+
{
46+
tasks.Add(Task.Run(() =>
47+
{
48+
for (int i = 0; i < allocationsPerThread; i++)
49+
{
50+
// Every thread allocates concurrently under zero-lock hot paths
51+
var handle = arena.Allocate(32);
52+
allHandles.Add(handle);
53+
54+
// Basic write verification to hit cache-lines simultaneously
55+
unsafe
56+
{
57+
int* ptr = (int*)arena.Resolve(handle);
58+
*ptr = Thread.CurrentThread.ManagedThreadId;
59+
}
60+
}
61+
}));
62+
}
63+
64+
// Wait for all worker threads to finish their flooding passes
65+
Task.WaitAll(tasks.ToArray());
66+
67+
// Assert
68+
Assert.AreEqual(threadCount * allocationsPerThread, allHandles.Count, "All requested allocations must succeed across threads.");
69+
70+
// Verify address uniqueness across the batch
71+
var uniqueAddresses = new HashSet<nint>();
72+
foreach (var handle in allHandles)
73+
{
74+
nint ptr = arena.Resolve(handle);
75+
Assert.IsTrue(uniqueAddresses.Add(ptr), $"Duplicate address collision discovered at pointer: {ptr}");
76+
77+
// Free the handles to verify concurrent deallocation pathways
78+
arena.Free(handle);
79+
}
80+
81+
arena.Dispose();
82+
}
83+
84+
/// <summary>
85+
/// Concurrency Test: Verifies the "Chaos Path" where Thread A allocates an object,
86+
/// hands it to Thread B, and Thread B calls Free().
87+
/// Ensures the handle safely passes through the ConcurrentQueue and is recycled by Thread A.
88+
/// </summary>
89+
[TestMethod]
90+
[TestCategory("Arena_Concurrency")]
91+
public unsafe void Arena_CrossThreadFree_RecyclesViaRemoteQueue()
92+
{
93+
// Arrange
94+
var config = new MemoryManagerConfig
95+
{
96+
FastLaneSize = 64 * 1024,
97+
SlowLaneSize = 256 * 1024,
98+
Threshold = 64,
99+
FastLaneStrategy = AllocatorStrategy.Slab // Slab allows us to easily track slot index recycling
100+
};
101+
102+
var arena = new ConcurrentMemoryArena(config);
103+
MemoryHandle sharedHandle = default;
104+
nint originalPointer = nint.Zero;
105+
106+
// ManualResetEvents to orchestrate our two worker threads cleanly
107+
using var allocationComplete = new ManualResetEvent(false);
108+
using var freeComplete = new ManualResetEvent(false);
109+
110+
// 1. Thread A: Allocates a 32-byte slot
111+
var threadA = new Thread(() =>
112+
{
113+
sharedHandle = arena.Allocate(32);
114+
originalPointer = arena.Resolve(sharedHandle);
115+
*(int*)originalPointer = 999;
116+
117+
allocationComplete.Set(); // Wake up Thread B
118+
freeComplete.WaitOne(); // Wait until Thread B frees our slot
119+
120+
// Thread A triggers a new allocation. This forces it to drain its remote inbox queue first!
121+
var recycledHandle = arena.Allocate(32);
122+
nint recycledPointer = arena.Resolve(recycledHandle);
123+
124+
// ASSERT: The slot must be perfectly recycled back onto Thread A's local stack context
125+
Assert.AreEqual(originalPointer, recycledPointer, "Thread A must successfully reclaim its slot after Thread B remote-freed it.");
126+
});
127+
128+
// 2. Thread B: Intercepts the handle and deletes it cross-thread lines
129+
var threadB = new Thread(() =>
130+
{
131+
allocationComplete.WaitOne(); // Wait until Thread A finishes allocation
132+
133+
// Verify the payload survived data transitions
134+
int currentVal = *(int*)arena.Resolve(sharedHandle);
135+
Assert.AreEqual(999, currentVal);
136+
137+
// CROSS-THREAD FREE: Thread B deletes memory it doesn't own
138+
arena.Free(sharedHandle);
139+
140+
freeComplete.Set(); // Wake up Thread A
141+
});
142+
143+
// Act
144+
threadA.Start();
145+
threadB.Start();
146+
147+
threadA.Join();
148+
threadB.Join();
149+
150+
// Cleanup
151+
arena.Dispose();
152+
}
153+
154+
/// <summary>
155+
/// Edge Case Test: Verifies that allocations exceeding the fast-lane threshold
156+
/// bypass thread-local memory structures completely and fall back cleanly into the
157+
/// thread-safe locked global SlowLane pool.
158+
/// </summary>
159+
[TestMethod]
160+
[TestCategory("Arena_Concurrency")]
161+
public void Arena_LargeAllocations_FallbackToGlobalSlowLane()
162+
{
163+
// Arrange
164+
var config = new MemoryManagerConfig
165+
{
166+
FastLaneSize = 64 * 1024,
167+
SlowLaneSize = 512 * 1024,
168+
Threshold = 128 // Allocations greater than 128 bytes fall down to SlowLane
169+
};
170+
171+
var arena = new ConcurrentMemoryArena(config);
172+
173+
// Act
174+
// Allocate 200 bytes (Exceeds 128 byte threshold)
175+
var largeHandle = arena.Allocate(200);
176+
177+
// Assert
178+
Assert.IsTrue(largeHandle.Id < 0, "Allocations shifting to the SlowLane must register with negative handle identifiers.");
179+
180+
var entry = arena.GetEntry(largeHandle);
181+
Assert.AreEqual(200, entry.Size);
182+
Assert.IsFalse(entry.IsStub);
183+
184+
// Clean verification pass
185+
arena.Free(largeHandle);
186+
arena.Dispose();
187+
}
188+
}
189+
}

MemoryManager.Tests/SlabLaneTests.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@
66
* PROGRAMMER: Peter Geinitz (Wayfarer)
77
*/
88

9-
using System;
10-
using System.Collections.Generic;
119
using MemoryManager.Core;
1210
using MemoryManager.Lanes;
13-
using Microsoft.VisualStudio.TestTools.UnitTesting;
1411

1512
namespace MemoryManager.Tests
1613
{

0 commit comments

Comments
 (0)