diff --git a/Misaki.HighPerformance.Jobs/JobScheduler.cs b/Misaki.HighPerformance.Jobs/JobScheduler.cs
index d165272..094cc0f 100644
--- a/Misaki.HighPerformance.Jobs/JobScheduler.cs
+++ b/Misaki.HighPerformance.Jobs/JobScheduler.cs
@@ -6,6 +6,15 @@ using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.Jobs;
+///
+/// Provides a mechanism for scheduling and executing jobs across multiple worker threads.
+///
+/// The class is designed to manage the execution of jobs, including support
+/// for dependencies, parallel execution, and thread-specific job assignment. It allows developers to schedule jobs that
+/// implement the or interfaces, and it ensures efficient utilization
+/// of worker threads through job batching and work-stealing mechanisms. This class is thread-safe and can be used in
+/// multi-threaded environments. However, it must be disposed when no longer needed to release resources and terminate
+/// worker threads.
public unsafe sealed class JobScheduler : IDisposable
{
private FreeList _jobDataAllocator;
@@ -23,6 +32,10 @@ public unsafe sealed class JobScheduler : IDisposable
internal bool IsCancellationRequested => _cts.IsCancellationRequested;
+ ///
+ /// Initializes a new instance of the class with the specified number of worker threads.
+ ///
+ /// The number of worker threads to create. If less than 1, at least one thread will be created.
public JobScheduler(int threadCount)
{
_jobDataAllocator = new(8);
@@ -333,7 +346,7 @@ public unsafe sealed class JobScheduler : IDisposable
///
/// A collection of instances representing the dependencies to combine.
/// A that represents the combined dependencies. The returned handle can be used to ensure
- /// that all specified dependencies are completed before proceeding.
+ /// that all specified dependencies are completed before proceeding.
public JobHandle CombineDependencies(params ReadOnlySpan dependencies)
{
var jobInfo = new JobInfo
@@ -350,6 +363,28 @@ public unsafe sealed class JobScheduler : IDisposable
return CreateJobHandle(ref jobInfo, dependencies);
}
+ ///
+ /// Retrieves the current status of a job identified by the specified handle.
+ ///
+ /// The handle representing the job whose status is to be retrieved. The handle must be valid.
+ /// The current status of the job as a value.
+ /// Returns if the handle is invalid or the job does not exist.
+ public JobStatus GetJobStatus(JobHandle handle)
+ {
+ if (!handle.IsValid)
+ {
+ return JobStatus.Invalid;
+ }
+
+ ref var jobInfo = ref _jobInfoPool.GetElementReferenceAt(handle._id, handle._generation, out var exist);
+ if (!exist)
+ {
+ return JobStatus.Invalid;
+ }
+
+ return (JobStatus)Volatile.Read(ref Unsafe.As(ref jobInfo.status));
+ }
+
///
/// Blocks the calling thread until the specified job is completed.
///
diff --git a/Misaki.HighPerformance.Jobs/WorkerThread.cs b/Misaki.HighPerformance.Jobs/WorkerThread.cs
index 9a3adcf..e030c56 100644
--- a/Misaki.HighPerformance.Jobs/WorkerThread.cs
+++ b/Misaki.HighPerformance.Jobs/WorkerThread.cs
@@ -29,23 +29,67 @@ internal class WorkerThread : IDisposable
public void Start() => _thread.Start();
+ private JobHandle FindJob()
+ {
+ var handle = JobHandle.Invalid;
+ if (_localQueue.TryDequeue(out handle)
+ || _scheduler.TryStealJob(-1, out handle))
+ {
+ return handle;
+ }
+
+ while (true)
+ {
+ var randomIndex = _random.Next(0, _scheduler.WorkerCount);
+ if (_scheduler.TryStealJob(randomIndex, out handle))
+ {
+ return handle;
+ }
+ }
+ }
+
private unsafe void WorkLoop()
{
while (!_scheduler.IsCancellationRequested)
{
- var handle = JobHandle.Invalid;
-
- // Always try the local thread and main thread queue first.
- if (!_localQueue.TryDequeue(out handle)
- && !_scheduler.TryStealJob(-1, out handle))
+ var spinner = new SpinWait();
+ for (var i = 0; i < 25; i++)
{
- var randomIndex = _random.Next(0, _scheduler.WorkerCount);
- if (_scheduler.TryStealJob(randomIndex, out var tempHandle))
+ spinner.SpinOnce(-1);
+
+ if (_scheduler.HasWork())
{
- handle = tempHandle;
+ // Instead of goto, we still need to go through the WaitForWork to claim a release.
+ // This causes lock and lots of branches inside the SemaphoreSlim, which lost 0.03ms.
+ // goto DoWork;
+ break;
}
}
+ try
+ {
+ _scheduler.WaitForWork();
+ }
+ catch (OperationCanceledException)
+ {
+ continue;
+ }
+
+ //var handle = JobHandle.Invalid;
+
+ //// Always try the local thread and main thread queue first.
+ //if (!_localQueue.TryDequeue(out handle)
+ // && !_scheduler.TryStealJob(-1, out handle))
+ //{
+ // var randomIndex = _random.Next(0, _scheduler.WorkerCount);
+ // if (_scheduler.TryStealJob(randomIndex, out var tempHandle))
+ // {
+ // handle = tempHandle;
+ // }
+ //}
+
+ //DoWork:
+ var handle = FindJob();
ref var jobInfo = ref _scheduler.GetJobInfoReference(handle, out var exist);
if (exist)
@@ -59,31 +103,6 @@ internal class WorkerThread : IDisposable
_scheduler.MarkJobComplete(handle);
}
}
- else
- {
- var spinner = new SpinWait();
- for (var i = 0; i < 25; i++)
- {
- spinner.SpinOnce(-1);
-
- if (_scheduler.HasWork())
- {
- goto FoundWork;
- }
- }
-
- try
- {
- _scheduler.WaitForWork();
- }
- catch (OperationCanceledException)
- {
- continue;
- }
- }
-
- FoundWork:
- ;
}
}
diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
index 71f684b..33d9e89 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
@@ -51,7 +51,6 @@ public readonly unsafe struct AllocationInfo
///
public static unsafe class AllocationManager
{
-
private unsafe struct ArenaAllocator : IAllocator, IDisposable
{
private DynamicArena _arena;
diff --git a/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs b/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs
index b0fafde..f0d6139 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/Arena .cs
@@ -49,15 +49,6 @@ public unsafe struct Arena : IDisposable
throw new ObjectDisposedException(nameof(DynamicArena));
}
- //var offset = _offset + alignment - 1 & ~(alignment - 1);
- //if (offset + size > _size)
- //{
- // return null;
- //}
-
- //_offset = offset + size;
- //var ptr = _buffer + offset;
-
nuint currentOffset, newOffset, alignedOffset;
do
diff --git a/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs b/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs
index 6b81320..fbe1f58 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/DynamicArena.cs
@@ -90,22 +90,6 @@ public unsafe struct DynamicArena : IDisposable
// Release the spinlock
Interlocked.Exchange(ref _nodeCreationLock, 0);
}
-
- //var newNode = (ArenaNode*)Malloc(SizeOf());
- //try
- //{
- // newNode->arena = new Arena(size);
- // newNode->next = null;
-
- // _current->next = newNode;
- // _current = newNode;
- // return true;
- //}
- //catch
- //{
- // Free(newNode);
- // return false;
- //}
}
///
@@ -162,9 +146,6 @@ public unsafe struct DynamicArena : IDisposable
_current = _root;
}
- ///
- /// Disposes all arenas and frees associated memory.
- ///
public void Dispose()
{
if (_root == null)
diff --git a/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs b/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs
index bea4983..f815129 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/FreeList.cs
@@ -6,25 +6,6 @@ namespace Misaki.HighPerformance.LowLevel.Buffer;
///
/// A lock-free, thread-safe variable-size allocator that manages memory blocks of different sizes.
/// Optimized for high-performance scenarios with frequent allocations and deallocations.
-///
-/// Example usage:
-///
-/// // Create a free list with multiple size buckets
-/// var freeList = new FreeList();
-///
-/// // Allocate a 70-byte block
-/// var block = freeList.Allocate(70);
-/// if (block.IsValid)
-/// {
-/// // Use the memory block...
-///
-/// // Free the block when done
-/// freeList.Free(block);
-/// }
-///
-/// // Dispose when finished
-/// freeList.Dispose();
-///
///
[StructLayout(LayoutKind.Explicit, Size = 256)] // Cache line aligned to prevent false sharing
public unsafe struct FreeList : IDisposable
@@ -476,10 +457,6 @@ public unsafe struct FreeList : IDisposable
}
}
- ///
- /// Disposes the free list and frees all allocated memory.
- /// Note: This method is NOT thread-safe by design as requested.
- ///
public void Dispose()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
diff --git a/Misaki.HighPerformance.LowLevel/Collections/CollectionHandle.cs b/Misaki.HighPerformance.LowLevel/Collections/CollectionHandle.cs
new file mode 100644
index 0000000..87033c4
--- /dev/null
+++ b/Misaki.HighPerformance.LowLevel/Collections/CollectionHandle.cs
@@ -0,0 +1,47 @@
+namespace Misaki.HighPerformance.LowLevel.Collections;
+
+public readonly struct CollectionHandle
+{
+ public readonly int id;
+ public readonly int generation;
+
+ public static CollectionHandle Invalid => new(-1, -1);
+
+ public bool IsValid => this != Invalid;
+
+ internal CollectionHandle(int id, int generation)
+ {
+ this.id = id;
+ this.generation = generation;
+ }
+
+ public bool Equals(CollectionHandle other)
+ {
+ return id == other.id && generation == other.generation;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is CollectionHandle handle && Equals(handle);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(id, generation);
+ }
+
+ public override string ToString()
+ {
+ return IsValid ? $"CollectionHandle({id}, {generation})" : "CollectionHandle(Invalid)";
+ }
+
+ public static bool operator ==(CollectionHandle left, CollectionHandle right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(CollectionHandle left, CollectionHandle right)
+ {
+ return !(left == right);
+ }
+}
\ No newline at end of file
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs
index f3fd2a2..e7760e4 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs
@@ -66,122 +66,6 @@ public unsafe struct UnsafeSparseSet : IUnsafeCollection
}
}
- public struct ParallelWriter
- {
- private UnsafeSparseSet* _sparseSet;
-
- internal ParallelWriter(UnsafeSparseSet* sparseSet)
- {
- _sparseSet = sparseSet;
- }
-
- ///
- /// Adds a value to the sparse set without resizing the internal arrays.
- ///
- /// The value to add to the sparse set.
- /// Returns the sparse index assigned to the value. -1 if the sparse index is out of bounds.
- public int AddNoResize(T value)
- {
- int sparseIndex;
-
- if (_sparseSet->_freeCount > 0)
- {
- var index = Interlocked.Decrement(ref _sparseSet->_freeCount);
- sparseIndex = _sparseSet->_freeList[index];
- }
- else
- {
- sparseIndex = Interlocked.Increment(ref _sparseSet->_nextId) - 1;
- }
-
- if (sparseIndex >= _sparseSet->_sparse.Count)
- {
- return -1;
- }
-
- var count = Interlocked.Increment(ref _sparseSet->_count) - 1;
-
- _sparseSet->_dense[count] = value;
- _sparseSet->_sparse[sparseIndex] = count;
- _sparseSet->_reverse[count] = sparseIndex;
-
-
- return sparseIndex;
- }
-
- ///
- /// Attempts to add a value at the specified sparse index without resizing the underlying collection.
- ///
- /// The index in the sparse array where the value should be added. Must be within the valid range of the sparse
- /// array.
- /// The value to add to the collection.
- /// if the value was successfully added at the specified index; otherwise, .
- public bool AddAtNoResize(int sparseIndex, T value)
- {
- if (sparseIndex < 0 || sparseIndex >= _sparseSet->_sparse.Count)
- {
- return false;
- }
-
- if (_sparseSet->Contains(sparseIndex))
- {
- return false;
- }
-
- if (_sparseSet->_count >= _sparseSet->_dense.Count)
- {
- return false;
- }
-
- var count = Interlocked.Increment(ref _sparseSet->_count) - 1;
-
- _sparseSet->_dense[count] = value;
- _sparseSet->_sparse[sparseIndex] = count;
- _sparseSet->_reverse[count] = sparseIndex;
-
- return true;
- }
-
- ///
- /// Removes a value at the specified sparse index without resizing the internal arrays.
- ///
- /// The sparse index of the value to remove.
- /// Returns if the value was successfully removed; otherwise, .
- public bool RemoveNoResize(int sparseIndex)
- {
- if (!_sparseSet->Contains(sparseIndex))
- {
- return false;
- }
-
- var denseIndex = _sparseSet->_sparse[sparseIndex];
- var lastIndex = _sparseSet->_count - 1;
-
- if (denseIndex != lastIndex)
- {
- var lastValue = _sparseSet->_dense[lastIndex];
- var lastSparseIndex = _sparseSet->_reverse[lastIndex];
- _sparseSet->_dense[denseIndex] = lastValue;
- _sparseSet->_reverse[denseIndex] = lastSparseIndex;
- _sparseSet->_sparse[lastSparseIndex] = denseIndex;
- }
-
- _sparseSet->_sparse[sparseIndex] = -1;
- if (_sparseSet->_freeCount >= _sparseSet->_freeList.Count)
- {
- return false;
- }
-
- _sparseSet->_freeList[_sparseSet->_freeCount] = sparseIndex;
-
- Interlocked.Increment(ref _sparseSet->_freeCount);
- Interlocked.Decrement(ref _sparseSet->_count);
-
- return true;
- }
- }
-
private UnsafeArray _dense;
private UnsafeArray _sparse;
private UnsafeArray _reverse; // Maps dense index to sparse index
@@ -194,12 +78,6 @@ public unsafe struct UnsafeSparseSet : IUnsafeCollection
public readonly int Capacity => _dense.Count;
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated && _freeList.IsCreated;
- public readonly ref T this[int index]
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => ref _dense[index];
- }
-
public IEnumerator GetEnumerator() => new Enumerator((UnsafeSparseSet*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
diff --git a/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeSparseSet.cs b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeSparseSet.cs
new file mode 100644
index 0000000..6448f96
--- /dev/null
+++ b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeSparseSet.cs
@@ -0,0 +1,46 @@
+using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Collections;
+
+namespace Misaki.HighPerformance.Test.UnitTest.Collections;
+
+[TestClass]
+public class TestUnsafeSparseSet
+{
+ private UnsafeSparseSet _sparseSet;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _sparseSet = new UnsafeSparseSet(16, Allocator.Persistent);
+ }
+
+ [TestMethod]
+ public void Add()
+ {
+ var id = _sparseSet.Add(10);
+ Assert.IsTrue(_sparseSet.Contains(id));
+ }
+
+ [TestMethod]
+ public void Remove()
+ {
+ var id = _sparseSet.Add(20);
+ Assert.IsTrue(_sparseSet.Contains(id));
+
+ _sparseSet.Remove(id);
+ Assert.IsFalse(_sparseSet.Contains(id));
+ }
+
+ [TestMethod]
+ public void IndexReuse()
+ {
+ var id = _sparseSet.Add(20);
+ Assert.IsTrue(_sparseSet.Contains(id));
+
+ _sparseSet.Remove(id);
+ Assert.IsFalse(_sparseSet.Contains(id));
+
+ var newId = _sparseSet.Add(30);
+ Assert.AreEqual(id, newId);
+ }
+}
\ No newline at end of file