Added UnsafeMultiHashMap
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
namespace Misaki.HighPerformance.Test.UnitTest.Collections;
|
||||
|
||||
[TestClass]
|
||||
public class TestUnsafeMultiHashMap
|
||||
{
|
||||
private UnsafeMultiHashMap<int, int> _multiHashMap;
|
||||
|
||||
[TestInitialize]
|
||||
public void Initialize()
|
||||
{
|
||||
_multiHashMap = new UnsafeMultiHashMap<int, int>(4, Allocator.Persistent);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
_multiHashMap.Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Add_AllowsDuplicateKeys()
|
||||
{
|
||||
_multiHashMap.Add(1, 10);
|
||||
_multiHashMap.Add(1, 20);
|
||||
_multiHashMap.Add(2, 30);
|
||||
_multiHashMap.Add(1, 40);
|
||||
|
||||
Assert.AreEqual(4, _multiHashMap.Count);
|
||||
Assert.IsTrue(_multiHashMap.ContainsKey(1));
|
||||
Assert.AreEqual(3, _multiHashMap.CountValuesForKey(1));
|
||||
|
||||
var values = new int[3];
|
||||
var count = 0;
|
||||
|
||||
Assert.IsTrue(_multiHashMap.TryGetFirstValue(1, out values[count++], out var iterator));
|
||||
|
||||
while (_multiHashMap.TryGetNextValue(out var value, ref iterator))
|
||||
{
|
||||
values[count++] = value;
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, count);
|
||||
Array.Sort(values);
|
||||
CollectionAssert.AreEqual(new[] { 10, 20, 40 }, values);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetValuesForKey_EnumeratesAllMatchingValues()
|
||||
{
|
||||
_multiHashMap.Add(7, 1);
|
||||
_multiHashMap.Add(7, 2);
|
||||
_multiHashMap.Add(3, 99);
|
||||
_multiHashMap.Add(7, 3);
|
||||
|
||||
var values = new int[3];
|
||||
var index = 0;
|
||||
|
||||
foreach (var value in _multiHashMap.GetValuesForKey(7))
|
||||
{
|
||||
values[index++] = value;
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, index);
|
||||
Array.Sort(values);
|
||||
CollectionAssert.AreEqual(new[] { 1, 2, 3 }, values);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Remove_RemovesAllValuesForKey()
|
||||
{
|
||||
_multiHashMap.Add(5, 10);
|
||||
_multiHashMap.Add(5, 20);
|
||||
_multiHashMap.Add(6, 30);
|
||||
|
||||
Assert.IsTrue(_multiHashMap.Remove(5));
|
||||
|
||||
Assert.AreEqual(1, _multiHashMap.Count);
|
||||
Assert.IsFalse(_multiHashMap.ContainsKey(5));
|
||||
Assert.AreEqual(0, _multiHashMap.CountValuesForKey(5));
|
||||
Assert.IsFalse(_multiHashMap.TryGetFirstValue(5, out _, out _));
|
||||
Assert.IsTrue(_multiHashMap.TryGetValue(6, out var remainingValue));
|
||||
Assert.AreEqual(30, remainingValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Clear_RemovesAllEntries()
|
||||
{
|
||||
_multiHashMap.Add(1, 10);
|
||||
_multiHashMap.Add(1, 20);
|
||||
_multiHashMap.Add(2, 30);
|
||||
|
||||
_multiHashMap.Clear();
|
||||
|
||||
Assert.AreEqual(0, _multiHashMap.Count);
|
||||
Assert.IsFalse(_multiHashMap.ContainsKey(1));
|
||||
Assert.IsFalse(_multiHashMap.ContainsKey(2));
|
||||
Assert.IsFalse(_multiHashMap.TryGetFirstValue(1, out _, out _));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Resize_PreservesDuplicateValues()
|
||||
{
|
||||
const int keyCount = 4;
|
||||
const int valuesPerKey = 8;
|
||||
|
||||
for (var key = 0; key < keyCount; key++)
|
||||
{
|
||||
for (var value = 0; value < valuesPerKey; value++)
|
||||
{
|
||||
_multiHashMap.Add(key, key * 100 + value);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.AreEqual(keyCount * valuesPerKey, _multiHashMap.Count);
|
||||
|
||||
for (var key = 0; key < keyCount; key++)
|
||||
{
|
||||
Assert.AreEqual(valuesPerKey, _multiHashMap.CountValuesForKey(key));
|
||||
|
||||
var values = new int[valuesPerKey];
|
||||
var index = 0;
|
||||
|
||||
Assert.IsTrue(_multiHashMap.TryGetFirstValue(key, out values[index++], out var iterator));
|
||||
while (_multiHashMap.TryGetNextValue(out var value, ref iterator))
|
||||
{
|
||||
values[index++] = value;
|
||||
}
|
||||
|
||||
Assert.AreEqual(valuesPerKey, index);
|
||||
Array.Sort(values);
|
||||
|
||||
var expected = new int[valuesPerKey];
|
||||
for (var value = 0; value < valuesPerKey; value++)
|
||||
{
|
||||
expected[value] = key * 100 + value;
|
||||
}
|
||||
|
||||
CollectionAssert.AreEqual(expected, values);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Enumerate_ReturnsEveryPair()
|
||||
{
|
||||
_multiHashMap.Add(1, 10);
|
||||
_multiHashMap.Add(1, 20);
|
||||
_multiHashMap.Add(2, 30);
|
||||
|
||||
var pairs = new List<KeyValuePair<int, int>>();
|
||||
foreach (var pair in _multiHashMap)
|
||||
{
|
||||
pairs.Add(pair);
|
||||
}
|
||||
|
||||
Assert.AreEqual(3, pairs.Count);
|
||||
CollectionAssert.AreEquivalent(
|
||||
new List<KeyValuePair<int, int>>
|
||||
{
|
||||
new(1, 10),
|
||||
new(1, 20),
|
||||
new(2, 30),
|
||||
},
|
||||
pairs);
|
||||
}
|
||||
}
|
||||
@@ -263,73 +263,6 @@ public unsafe class TestJobSystem
|
||||
Assert.AreEqual(JobState.Completed, s_jobScheduler.GetJobStatus(completedHandle));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RaceConditionTest()
|
||||
{
|
||||
const int jobCount = 20000;
|
||||
|
||||
var pExecutedCount = (int*)NativeMemory.Alloc(sizeof(int));
|
||||
*pExecutedCount = 0;
|
||||
|
||||
var startSignal = false;
|
||||
|
||||
// 1. Create a "Gatekeeper" vectorJob that spins/blocks a worker thread until signaled.
|
||||
// This allows us to control exactly when the dependency completes.
|
||||
var rootJob = new WaitJob { pSignal = &startSignal };
|
||||
var rootHandle = s_jobScheduler.Schedule(ref rootJob);
|
||||
|
||||
// 2. Start a background task to flood the scheduler with dependencies on the Gatekeeper.
|
||||
using var barrier = new Barrier(2);
|
||||
var scheduleTask = Task.Run(() =>
|
||||
{
|
||||
var depJob = new IncrementJob { pCounter = pExecutedCount };
|
||||
barrier.SignalAndWait(TestContext.CancellationTokenSource.Token); // Synchronize start with main thread
|
||||
|
||||
for (var i = 0; i < jobCount; i++)
|
||||
{
|
||||
// CONTENTION POINT:
|
||||
// Trying to add a dependency to 'rootHandle'.
|
||||
// Eventually, this will happen exactly while 'rootHandle' is transitioning to Completed.
|
||||
s_jobScheduler.Schedule(ref depJob, rootHandle);
|
||||
}
|
||||
}, TestContext.CancellationTokenSource.Token);
|
||||
|
||||
barrier.SignalAndWait(TestContext.CancellationTokenSource.Token); // Wait for scheduler task to be ready
|
||||
|
||||
// Allow the scheduling loop to get a head start and queue some readers
|
||||
Thread.Sleep(5);
|
||||
|
||||
// 3. Open the gate.
|
||||
// This triggers the Gatekeeper to complete. It will change its State and iterate its dependency list.
|
||||
// This happens CONCURRENTLY with the loop above adding more items to that same list.
|
||||
startSignal = true;
|
||||
|
||||
scheduleTask.Wait(TestContext.CancellationTokenSource.Token);
|
||||
|
||||
// 4. Validate results
|
||||
// If the lock-free logic works, every single dependent vectorJob must eventually execute.
|
||||
// If there is a race (e.g., missed notification), pExecutedCount will stick below jobCount.
|
||||
var spin = new SpinWait();
|
||||
var timeout = DateTime.Now.AddSeconds(10);
|
||||
|
||||
while (Volatile.Read(ref *pExecutedCount) < jobCount)
|
||||
{
|
||||
if (DateTime.Now > timeout)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
spin.SpinOnce();
|
||||
}
|
||||
|
||||
// Ensure the root vectorJob is officially cleaned up
|
||||
s_jobScheduler.Wait(rootHandle);
|
||||
|
||||
Assert.AreEqual(jobCount, *pExecutedCount, "Race condition detected: Some dependent jobs failed to execute (Wait timeout).");
|
||||
|
||||
NativeMemory.Free(pExecutedCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SPMDCorrectness()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user