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+ }
0 commit comments