diff --git a/Runtime/ABase/Structs/TypeNamePair.cs b/Runtime/ABase/Structs/TypeNamePair.cs
index 4ec532e..5afcfef 100644
--- a/Runtime/ABase/Structs/TypeNamePair.cs
+++ b/Runtime/ABase/Structs/TypeNamePair.cs
@@ -99,7 +99,7 @@ namespace AlicizaX
/// 被比较的对象是否与自身相等。
public bool Equals(TypeNamePair value)
{
- return m_Type == value.m_Type && m_Name == value.m_Name;
+ return m_Type == value.m_Type && string.Equals(m_Name, value.m_Name, StringComparison.Ordinal);
}
///
diff --git a/Runtime/Audio/AudioSourceObject.cs b/Runtime/Audio/AudioSourceObject.cs
index 968387e..7eb969a 100644
--- a/Runtime/Audio/AudioSourceObject.cs
+++ b/Runtime/Audio/AudioSourceObject.cs
@@ -4,7 +4,7 @@ using UnityEngine;
namespace AlicizaX.Audio.Runtime
{
- internal sealed class AudioSourceObject : ObjectBase
+ internal sealed class AudioSourceObject : ObjectBase
{
private AudioSource _source;
private AudioLowPassFilter _lowPassFilter;
diff --git a/Runtime/Debugger/DebuggerComponent.ObjectPoolInformationWindow.cs b/Runtime/Debugger/DebuggerComponent.ObjectPoolInformationWindow.cs
index 79f29ee..770210b 100644
--- a/Runtime/Debugger/DebuggerComponent.ObjectPoolInformationWindow.cs
+++ b/Runtime/Debugger/DebuggerComponent.ObjectPoolInformationWindow.cs
@@ -9,14 +9,14 @@ namespace AlicizaX.Debugger.Runtime
private sealed class ObjectPoolInformationWindow : PollingDebuggerWindowBase
{
private IObjectPoolService m_ObjectPoolService;
- private IObjectPoolServiceDebugView m_ObjectPoolDebugView;
+ private ObjectPoolService m_ObjectPoolServiceImpl;
private ObjectPoolBase[] m_ObjectPools;
private ObjectInfo[] m_ObjectInfos;
public override void Initialize(params object[] args)
{
m_ObjectPoolService = AppServices.Require();
- m_ObjectPoolDebugView = m_ObjectPoolService as IObjectPoolServiceDebugView;
+ m_ObjectPoolServiceImpl = m_ObjectPoolService as ObjectPoolService;
}
protected override void BuildWindow(VisualElement root)
@@ -31,8 +31,8 @@ namespace AlicizaX.Debugger.Runtime
root.Add(overview);
int objectPoolCount = EnsureObjectPoolBuffer(m_ObjectPoolService.Count);
- objectPoolCount = m_ObjectPoolDebugView != null
- ? m_ObjectPoolDebugView.GetAllObjectPools(true, m_ObjectPools)
+ objectPoolCount = m_ObjectPoolServiceImpl != null
+ ? m_ObjectPoolServiceImpl.GetAllObjectPools(true, m_ObjectPools)
: 0;
for (int i = 0; i < objectPoolCount; i++)
{
diff --git a/Runtime/MemoryPool/Benchmark.meta b/Runtime/MemoryPool/Benchmark.meta
new file mode 100644
index 0000000..aae09bd
--- /dev/null
+++ b/Runtime/MemoryPool/Benchmark.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0cd45b8170524028b8e9d569c3bb5b9d
+timeCreated: 1777270000
diff --git a/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs b/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs
new file mode 100644
index 0000000..d5346f6
--- /dev/null
+++ b/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs
@@ -0,0 +1,814 @@
+#if UNITY_EDITOR
+using System;
+using System.Diagnostics;
+using Cysharp.Text;
+using Unity.Profiling;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace AlicizaX
+{
+ [DisallowMultipleComponent]
+ [AddComponentMenu("Game Framework/MemoryPool Benchmark")]
+ public sealed class MemoryPoolBenchmark : MonoBehaviour
+ {
+ [SerializeField] private bool runOnStart = true;
+ [SerializeField] private int objectCount = 10000;
+ [SerializeField] private int loopCount = 100000;
+ [SerializeField] private int adaptiveFrameCount = 420;
+ [SerializeField] private int burstSize = 4096;
+ [SerializeField] private int extremeBurstSize = 32768;
+ [SerializeField] private int waveCount = 24;
+ [SerializeField] private int multiTypeCount = 2048;
+ [SerializeField] private bool logEachCase = true;
+ [SerializeField] private bool logMemoryDelta = true;
+ [SerializeField] private int maxCapturedLogChars = 128 * 1024;
+
+ private static readonly ProfilerMarker s_TotalMarker = new ProfilerMarker("MemoryPoolBenchmark.Total");
+ private static readonly ProfilerMarker s_SimpleMarker = new ProfilerMarker("MemoryPoolBenchmark.Simple");
+ private static readonly ProfilerMarker s_AcquireReleaseMarker = new ProfilerMarker("MemoryPoolBenchmark.AcquireRelease");
+ private static readonly ProfilerMarker s_AdaptiveMarker = new ProfilerMarker("MemoryPoolBenchmark.AdaptivePolicy");
+ private static readonly ProfilerMarker s_ExtremeMarker = new ProfilerMarker("MemoryPoolBenchmark.Extreme");
+ private static readonly ProfilerMarker s_InfoMarker = new ProfilerMarker("MemoryPoolBenchmark.InfoBuffer");
+ private static readonly ProfilerMarker s_CompactMarker = new ProfilerMarker("MemoryPoolBenchmark.Compact");
+
+ private readonly Stopwatch m_Stopwatch = new Stopwatch();
+ private Utf16ValueStringBuilder m_LogBuilder;
+ private bool m_LogBuilderCreated;
+ private int m_FailCount;
+ private int m_CaseCount;
+ private long m_CaseAllocBefore;
+ private long m_CaseAllocAfter;
+ private BenchmarkMemory[] m_Buffer;
+ private SimpleMemory[] m_SimpleBuffer;
+ private BenchmarkMemoryA[] m_BufferA;
+ private BenchmarkMemoryB[] m_BufferB;
+ private BenchmarkMemoryC[] m_BufferC;
+ private MemoryPoolInfo[] m_InfoBuffer;
+
+ private void OnEnable()
+ {
+ ClearCapturedConsoleOutput();
+ Application.logMessageReceived += OnLogMessageReceived;
+ }
+
+ private void OnDisable()
+ {
+ Application.logMessageReceived -= OnLogMessageReceived;
+ m_LogBuilder.Dispose();
+ m_LogBuilderCreated = false;
+ }
+
+ private void Start()
+ {
+ if (runOnStart)
+ RunAll();
+ }
+
+ [ContextMenu("Run MemoryPool Benchmark")]
+ public void RunAll()
+ {
+ ClearCapturedConsoleOutput();
+ m_FailCount = 0;
+ m_CaseCount = 0;
+ int maxBuffer = Math.Max(Math.Max(objectCount, burstSize), extremeBurstSize);
+ EnsureBuffer(maxBuffer);
+ EnsureSimpleBuffer(64);
+ EnsureTypedBuffers(Math.Max(multiTypeCount, burstSize));
+
+ using (s_TotalMarker.Auto())
+ {
+ RunCase("Simple Acquire/Release", RunSimpleAcquireRelease);
+ RunCase("Simple Reuse Identity", RunSimpleReuseIdentity);
+ RunCase("Simple Capacity Learning", RunSimpleCapacityLearning);
+ RunCase("Acquire/Release Hot Loop", RunAcquireReleaseHotLoop);
+ RunCase("Interleaved Acquire Release", RunInterleavedAcquireRelease);
+ RunCase("Generic API Hot Loop", RunGenericApiHotLoop);
+ RunCase("Facade Generic Release Hot Loop", RunFacadeGenericReleaseHotLoop);
+ RunCase("Facade Acquire Direct Release", RunFacadeAcquireDirectRelease);
+ RunCase("Direct Acquire Facade Release", RunDirectAcquireFacadeRelease);
+ RunCase("Adaptive Burst Fill", RunAdaptiveBurstFill);
+ RunCase("Idle Budget Release", RunIdleBudgetRelease);
+ RunCase("Wave Burst Anti Thrash", RunWaveBurstAntiThrash);
+ RunCase("Extreme Single Burst", RunExtremeSingleBurst);
+ RunCase("Extreme Hard Capacity Overflow", RunExtremeHardCapacityOverflow);
+ RunCase("Multi Type Active Queue", RunMultiTypeActiveQueue);
+ RunCase("ClearAll Unschedule", RunClearAllUnschedule);
+ RunCase("ClearAll Active Queue Reset", RunClearAllActiveQueueReset);
+ RunCase("Type API Cold Path", RunTypeApiColdPath);
+ RunCase("Info Buffer No Alloc", RunInfoBufferNoAlloc);
+ RunCase("Explicit Compact", RunExplicitCompact);
+ }
+
+ Debug.Log(BuildLog("MemoryPool benchmark finished. cases=", m_CaseCount, ", fails=", m_FailCount));
+ }
+
+ [ContextMenu("Copy Captured Console Output")]
+ public void CopyCapturedConsoleOutput()
+ {
+ EnsureLogBuilder();
+ string text = m_LogBuilder.ToString();
+ GUIUtility.systemCopyBuffer = text;
+ Debug.Log(BuildLog("MemoryPoolBenchmark copied console output chars=", text.Length, ", max=", maxCapturedLogChars));
+ }
+
+ [ContextMenu("Clear Captured Console Output")]
+ public void ClearCapturedConsoleOutput()
+ {
+ m_LogBuilder.Dispose();
+ m_LogBuilder = ZString.CreateStringBuilder();
+ m_LogBuilderCreated = true;
+ }
+
+ private void EnsureLogBuilder()
+ {
+ if (!m_LogBuilderCreated)
+ {
+ m_LogBuilder = ZString.CreateStringBuilder();
+ m_LogBuilderCreated = true;
+ }
+ }
+
+ private void OnLogMessageReceived(string condition, string stackTrace, LogType type)
+ {
+ EnsureLogBuilder();
+ if (m_LogBuilder.Length >= maxCapturedLogChars)
+ return;
+
+ m_LogBuilder.Append('[');
+ m_LogBuilder.Append(type);
+ m_LogBuilder.Append("] ");
+ m_LogBuilder.Append(condition);
+ m_LogBuilder.AppendLine();
+
+ if (type == LogType.Exception || type == LogType.Error || type == LogType.Assert)
+ {
+ m_LogBuilder.Append(stackTrace);
+ m_LogBuilder.AppendLine();
+ }
+ }
+
+ private void RunCase(string caseName, Action action)
+ {
+ m_CaseCount++;
+ m_CaseAllocBefore = GetAllocatedBytesForCurrentThread();
+ m_CaseAllocAfter = m_CaseAllocBefore;
+ m_Stopwatch.Restart();
+ action();
+ if (m_Stopwatch.IsRunning)
+ {
+ m_Stopwatch.Stop();
+ m_CaseAllocAfter = GetAllocatedBytesForCurrentThread();
+ }
+
+ if (!logEachCase)
+ return;
+
+ if (logMemoryDelta)
+ Debug.Log(BuildLog("[MemoryPoolBenchmark] ", caseName, " ms=", m_Stopwatch.Elapsed.TotalMilliseconds, " gcAlloc=", m_CaseAllocAfter - m_CaseAllocBefore));
+ else
+ Debug.Log(BuildLog("[MemoryPoolBenchmark] ", caseName, " ms=", m_Stopwatch.Elapsed.TotalMilliseconds));
+ }
+
+ private void RestartCaseMeasure()
+ {
+ m_CaseAllocBefore = GetAllocatedBytesForCurrentThread();
+ m_CaseAllocAfter = m_CaseAllocBefore;
+ m_Stopwatch.Restart();
+ }
+
+ private void StopCaseMeasure()
+ {
+ m_Stopwatch.Stop();
+ m_CaseAllocAfter = GetAllocatedBytesForCurrentThread();
+ }
+
+ private long GetAllocatedBytesForCurrentThread()
+ {
+ return logMemoryDelta ? GC.GetAllocatedBytesForCurrentThread() : 0L;
+ }
+
+ private void RunSimpleAcquireRelease()
+ {
+ using (s_SimpleMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.Prewarm(1);
+
+ RestartCaseMeasure();
+ SimpleMemory item = MemoryPool.Acquire();
+ item.Value = 7;
+ MemoryPool.Release(item);
+ StopCaseMeasure();
+
+ AssertEqual(item.Value, 0, "simple release did not clear object");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunSimpleReuseIdentity()
+ {
+ using (s_SimpleMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.Prewarm(1);
+ SimpleMemory first = MemoryPool.Acquire();
+ MemoryPool.Release(first);
+
+ RestartCaseMeasure();
+ SimpleMemory second = MemoryPool.Acquire();
+ StopCaseMeasure();
+
+ AssertTrue(ReferenceEquals(first, second), "simple reuse did not return same instance");
+ MemoryPool.Release(second);
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunSimpleCapacityLearning()
+ {
+ using (s_SimpleMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(64, 256);
+ for (int i = 0; i < 48; i++)
+ m_SimpleBuffer[i] = null;
+
+ RestartCaseMeasure();
+ for (int i = 0; i < 48; i++)
+ m_SimpleBuffer[i] = MemoryPool.Acquire();
+ StopCaseMeasure();
+
+ for (int i = 0; i < 48; i++)
+ {
+ MemoryPool.Release(m_SimpleBuffer[i]);
+ m_SimpleBuffer[i] = null;
+ }
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunAcquireReleaseHotLoop()
+ {
+ using (s_AcquireReleaseMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(objectCount, objectCount << 2);
+ MemoryPool.Prewarm(objectCount);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkMemory item = MemoryPool.Acquire();
+ item.Value = i;
+ MemoryPool.Release(item);
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunInterleavedAcquireRelease()
+ {
+ using (s_AcquireReleaseMarker.Auto())
+ {
+ int count = Math.Min(objectCount, m_Buffer.Length);
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(count, count << 2);
+ MemoryPool.Prewarm(count >> 1);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < count; i++)
+ {
+ BenchmarkMemory item = MemoryPool.Acquire();
+ item.Value = i;
+ if ((i & 1) == 0)
+ {
+ MemoryPool.Release(item);
+ }
+ else
+ {
+ m_Buffer[i] = item;
+ }
+ }
+
+ for (int i = 1; i < count; i += 2)
+ {
+ MemoryPool.Release(m_Buffer[i]);
+ m_Buffer[i] = null;
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunGenericApiHotLoop()
+ {
+ using (s_AcquireReleaseMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(objectCount, objectCount << 2);
+ MemoryPool.Prewarm(objectCount);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkMemory item = MemoryPool.Acquire();
+ MemoryPool.Release(item);
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+
+ private void RunFacadeGenericReleaseHotLoop()
+ {
+ using (s_AcquireReleaseMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(objectCount, objectCount << 2);
+ MemoryPool.Prewarm(objectCount);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkMemory item = MemoryPool.Acquire();
+ MemoryPool.Release(item);
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+
+ private void RunFacadeAcquireDirectRelease()
+ {
+ using (s_AcquireReleaseMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(objectCount, objectCount << 2);
+ MemoryPool.Prewarm(objectCount);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkMemory item = MemoryPool.Acquire();
+ MemoryPool.Release(item);
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunDirectAcquireFacadeRelease()
+ {
+ using (s_AcquireReleaseMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(objectCount, objectCount << 2);
+ MemoryPool.Prewarm(objectCount);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkMemory item = MemoryPool.Acquire();
+ MemoryPool.Release(item);
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunAdaptiveBurstFill()
+ {
+ using (s_AdaptiveMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(Math.Max(64, burstSize >> 1), burstSize << 1);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < burstSize; i++)
+ m_Buffer[i] = MemoryPool.Acquire();
+
+ for (int i = 0; i < burstSize; i++)
+ {
+ MemoryPool.Release(m_Buffer[i]);
+ m_Buffer[i] = null;
+ }
+
+ for (int frame = 0; frame < adaptiveFrameCount; frame++)
+ MemoryPoolRegistry.TickAll(frame);
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.UnusedCount > 0, "adaptive fill did not keep reserve");
+ AssertTrue(info.PoolArrayLength >= info.UnusedCount, "pool array smaller than unused count");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunIdleBudgetRelease()
+ {
+ using (s_AdaptiveMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(burstSize, burstSize << 1);
+ MemoryPool.Prewarm(burstSize);
+
+ RestartCaseMeasure();
+ for (int frame = 0; frame < adaptiveFrameCount + 360; frame++)
+ MemoryPoolRegistry.TickAll(frame + 10000);
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.UnusedCount < burstSize, "idle release did not reduce unused objects");
+ AssertTrue(info.PoolArrayLength >= burstSize, "idle release should not compact backing array");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunWaveBurstAntiThrash()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ int count = Math.Min(burstSize, m_Buffer.Length);
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(count, count << 1);
+
+ RestartCaseMeasure();
+ for (int wave = 0; wave < waveCount; wave++)
+ {
+ int waveSize = (wave & 1) == 0 ? count : count >> 2;
+ for (int i = 0; i < waveSize; i++)
+ m_Buffer[i] = MemoryPool.Acquire();
+
+ for (int i = 0; i < waveSize; i++)
+ {
+ MemoryPool.Release(m_Buffer[i]);
+ m_Buffer[i] = null;
+ }
+
+ for (int frame = 0; frame < 12; frame++)
+ MemoryPoolRegistry.TickAll(20000 + wave * 16 + frame);
+ }
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.PoolArrayLength >= count, "wave burst backing array shrank unexpectedly");
+ AssertTrue(info.UnusedCount > 0, "wave burst failed to retain reserve");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunExtremeSingleBurst()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ int count = Math.Min(extremeBurstSize, m_Buffer.Length);
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(Math.Max(128, count >> 2), count);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < count; i++)
+ m_Buffer[i] = MemoryPool.Acquire();
+
+ for (int i = 0; i < count; i++)
+ {
+ MemoryPool.Release(m_Buffer[i]);
+ m_Buffer[i] = null;
+ }
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.UnusedCount == count, "extreme single burst did not keep released objects under hard cap");
+ AssertTrue(info.PoolArrayLength >= count, "extreme single burst did not grow backing array");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunExtremeHardCapacityOverflow()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ int count = Math.Min(burstSize, m_Buffer.Length);
+ int hardCapacity = Math.Max(32, count >> 3);
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(hardCapacity >> 1, hardCapacity);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < count; i++)
+ m_Buffer[i] = MemoryPool.Acquire();
+
+ for (int i = 0; i < count; i++)
+ {
+ MemoryPool.Release(m_Buffer[i]);
+ m_Buffer[i] = null;
+ }
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.UnusedCount == hardCapacity, "hard capacity overflow retained more than hard cap");
+ AssertTrue(info.PoolArrayLength == hardCapacity, "hard capacity overflow grew array past hard cap");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunMultiTypeActiveQueue()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ int count = Math.Min(multiTypeCount, m_BufferA.Length);
+ MemoryPool.ClearAll();
+ MemoryPool.ClearAll();
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(count, count << 1);
+ MemoryPool.SetCapacity(count, count << 1);
+ MemoryPool.SetCapacity(count, count << 1);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < count; i++)
+ {
+ m_BufferA[i] = MemoryPool.Acquire();
+ m_BufferB[i] = MemoryPool.Acquire();
+ m_BufferC[i] = MemoryPool.Acquire();
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ MemoryPool.Release(m_BufferA[i]);
+ MemoryPool.Release(m_BufferB[i]);
+ MemoryPool.Release(m_BufferC[i]);
+ m_BufferA[i] = null;
+ m_BufferB[i] = null;
+ m_BufferC[i] = null;
+ }
+
+ for (int frame = 0; frame < adaptiveFrameCount; frame++)
+ MemoryPoolRegistry.TickAll(30000 + frame);
+ StopCaseMeasure();
+
+ AssertTrue(GetBenchmarkInfo(typeof(BenchmarkMemoryA)).UnusedCount > 0, "type A did not tick");
+ AssertTrue(GetBenchmarkInfo(typeof(BenchmarkMemoryB)).UnusedCount > 0, "type B did not tick");
+ AssertTrue(GetBenchmarkInfo(typeof(BenchmarkMemoryC)).UnusedCount > 0, "type C did not tick");
+ MemoryPool.ClearAll();
+ MemoryPool.ClearAll();
+ MemoryPool.ClearAll();
+ }
+ }
+
+
+ private void RunClearAllUnschedule()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(burstSize, burstSize << 1);
+ MemoryPool.Prewarm(burstSize);
+ MemoryPoolRegistry.TickAll(39000);
+ MemoryPool.ClearAll();
+
+ RestartCaseMeasure();
+ for (int frame = 0; frame < adaptiveFrameCount; frame++)
+ MemoryPoolRegistry.TickAll(39001 + frame);
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.UnusedCount == 0, "clear all should unschedule single pool tick");
+ AssertTrue(info.PoolArrayLength == 0, "clear all should keep backing array empty until reused");
+ }
+ }
+
+ private void RunClearAllActiveQueueReset()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(burstSize, burstSize << 1);
+ MemoryPool.Prewarm(burstSize);
+ MemoryPoolRegistry.TickAll(40000);
+ MemoryPoolRegistry.ClearAll();
+
+ RestartCaseMeasure();
+ MemoryPool.Prewarm(16);
+ MemoryPoolRegistry.TickAll(40001);
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.UnusedCount >= 16, "clear all active queue reset blocked reschedule");
+ AssertTrue(info.PoolArrayLength >= 16, "clear all active queue reset did not regrow backing array");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunTypeApiColdPath()
+ {
+ using (s_ExtremeMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.Prewarm(objectCount);
+ Type type = typeof(BenchmarkMemory);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ IMemory item = MemoryPool.Acquire(type);
+ MemoryPool.Release(item);
+ }
+ StopCaseMeasure();
+
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private void RunInfoBufferNoAlloc()
+ {
+ using (s_InfoMarker.Auto())
+ {
+ EnsureInfoBuffer(Math.Max(1, MemoryPool.Count));
+
+ RestartCaseMeasure();
+ int count = MemoryPool.GetAllMemoryPoolInfos(m_InfoBuffer);
+ StopCaseMeasure();
+
+ AssertTrue(count <= m_InfoBuffer.Length, "info count exceeded buffer length");
+ }
+ }
+
+ private void RunExplicitCompact()
+ {
+ using (s_CompactMarker.Auto())
+ {
+ MemoryPool.ClearAll();
+ MemoryPool.SetCapacity(objectCount, objectCount << 2);
+ MemoryPool.Prewarm(objectCount);
+ MemoryPool.Shrink(8);
+
+ RestartCaseMeasure();
+ MemoryPool.Compact();
+ StopCaseMeasure();
+
+ MemoryPoolInfo info = GetBenchmarkInfo(typeof(BenchmarkMemory));
+ AssertTrue(info.PoolArrayLength <= 8, "compact did not shrink backing array");
+ MemoryPool.ClearAll();
+ }
+ }
+
+ private MemoryPoolInfo GetBenchmarkInfo(Type targetType)
+ {
+ EnsureInfoBuffer(Math.Max(1, MemoryPool.Count));
+ int count = MemoryPool.GetAllMemoryPoolInfos(m_InfoBuffer);
+ for (int i = 0; i < count; i++)
+ {
+ if (m_InfoBuffer[i].Type == targetType)
+ return m_InfoBuffer[i];
+ }
+
+ return default;
+ }
+
+ private void EnsureBuffer(int count)
+ {
+ if (m_Buffer == null || m_Buffer.Length < count)
+ m_Buffer = new BenchmarkMemory[count];
+ }
+
+ private void EnsureSimpleBuffer(int count)
+ {
+ if (m_SimpleBuffer == null || m_SimpleBuffer.Length < count)
+ m_SimpleBuffer = new SimpleMemory[count];
+ }
+
+ private void EnsureTypedBuffers(int count)
+ {
+ if (m_BufferA == null || m_BufferA.Length < count)
+ m_BufferA = new BenchmarkMemoryA[count];
+ if (m_BufferB == null || m_BufferB.Length < count)
+ m_BufferB = new BenchmarkMemoryB[count];
+ if (m_BufferC == null || m_BufferC.Length < count)
+ m_BufferC = new BenchmarkMemoryC[count];
+ }
+
+ private void EnsureInfoBuffer(int count)
+ {
+ if (m_InfoBuffer == null || m_InfoBuffer.Length < count)
+ m_InfoBuffer = new MemoryPoolInfo[count];
+ }
+
+ private void AssertTrue(bool value, string message)
+ {
+ if (value)
+ return;
+
+ m_FailCount++;
+ Debug.LogError(message);
+ }
+
+ private void AssertEqual(int actual, int expected, string message)
+ {
+ if (actual == expected)
+ return;
+
+ m_FailCount++;
+ Debug.LogError(BuildLog(message, " actual=", actual, ", expected=", expected));
+ }
+
+ private static string BuildLog(object a, string b, object c, string d, object e)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(a);
+ builder.Append(b);
+ builder.Append(c);
+ builder.Append(d);
+ builder.Append(e);
+ return builder.ToString();
+ }
+ }
+
+ private static string BuildLog(string a, object b, string c, object d)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(a);
+ builder.Append(b);
+ builder.Append(c);
+ builder.Append(d);
+ return builder.ToString();
+ }
+ }
+
+ private static string BuildLog(string a, object b, string c, object d, string e, object f)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(a);
+ builder.Append(b);
+ builder.Append(c);
+ builder.Append(d);
+ builder.Append(e);
+ builder.Append(f);
+ return builder.ToString();
+ }
+ }
+
+ private sealed class SimpleMemory : IMemory
+ {
+ public int Value;
+
+ public void Clear()
+ {
+ Value = 0;
+ }
+ }
+
+ private sealed class BenchmarkMemory : IMemory
+ {
+ public int Value;
+
+ public void Clear()
+ {
+ Value = 0;
+ }
+ }
+
+ private sealed class BenchmarkMemoryA : IMemory
+ {
+ public int Value;
+
+ public void Clear()
+ {
+ Value = 0;
+ }
+ }
+
+ private sealed class BenchmarkMemoryB : IMemory
+ {
+ public int Value;
+
+ public void Clear()
+ {
+ Value = 0;
+ }
+ }
+
+ private sealed class BenchmarkMemoryC : IMemory
+ {
+ public int Value;
+
+ public void Clear()
+ {
+ Value = 0;
+ }
+ }
+ }
+}
+#endif
+
diff --git a/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs.meta b/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs.meta
new file mode 100644
index 0000000..f40135c
--- /dev/null
+++ b/Runtime/MemoryPool/Benchmark/MemoryPoolBenchmark.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 80a2bc6a495f4a1cb6ffb632fda36376
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/MemoryPool/MemoryPool.Core.cs b/Runtime/MemoryPool/MemoryPool.Core.cs
index 3884d98..8242924 100644
--- a/Runtime/MemoryPool/MemoryPool.Core.cs
+++ b/Runtime/MemoryPool/MemoryPool.Core.cs
@@ -4,7 +4,6 @@ using System.Runtime.CompilerServices;
namespace AlicizaX
{
-
public static class MemoryPool where T : class, IMemory, new()
{
private sealed class ReferenceComparer : IEqualityComparer
@@ -24,40 +23,46 @@ namespace AlicizaX
}
}
+ private static readonly MemoryPoolRegistry.MemoryPoolHandle s_Handle;
private static T[] s_Stack = Array.Empty();
private static int s_Count;
- private static int s_MaxCapacity = 2048;
+ private static int s_SoftCapacity = 2048;
+ private static int s_HardCapacity = 8192;
private static Dictionary s_InPoolSet;
private static int s_StrictCheckVersion = -1;
- // ---- 回收策略状态 ----
- private static int s_HighWaterMark;
- private static int s_RecentAcquireCount;
- private static int s_IdleFrames;
- private static int s_LastTickFrame;
- private static int s_PeakInUse;
private static int s_CurrentInUse;
+ private static int s_PeakInUseShort;
+ private static int s_PeakInUseLong;
+ private static int s_AcquireThisFrame;
+ private static int s_ReleaseThisFrame;
+ private static int s_TargetReserve = MIN_KEEP;
+ private static int s_IdleFrames;
+ private static int s_HotFrames;
+ private static int s_LastTickFrame = -1;
+ private static int s_ConsecutiveMiss;
- private const int IDLE_THRESHOLD = 300; // ~5s @60fps
- private const int IDLE_AGGRESSIVE = 900; // ~15s @60fps
private const int MIN_KEEP = 4;
+ private const int SHORT_DECAY_START = 300;
+ private const int LONG_DECAY_START = 1800;
+ private const int UNSCHEDULE_IDLE_FRAMES = 3600;
- // ---- 统计计数器 ----
private static int s_AcquireCount;
private static int s_ReleaseCount;
private static int s_CreateCount;
static MemoryPool()
{
- MemoryPoolRegistry.Register(typeof(T), new MemoryPoolRegistry.MemoryPoolHandle(
+ s_Handle = new MemoryPoolRegistry.MemoryPoolHandle(
acquire: AcquireAsMemory,
release: ReleaseAsMemory,
clear: ClearAll,
prewarm: Prewarm,
getInfo: GetInfo,
tick: Tick,
- shrink: Shrink
- ));
+ shrink: Shrink,
+ compact: Compact);
+ MemoryPoolRegistry.Register(typeof(T), s_Handle);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -75,14 +80,18 @@ namespace AlicizaX
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Acquire()
{
+ MemoryPoolRegistry.ScheduleTick(s_Handle);
s_AcquireCount++;
- s_RecentAcquireCount++;
+ s_AcquireThisFrame++;
s_CurrentInUse++;
- if (s_CurrentInUse > s_PeakInUse)
- s_PeakInUse = s_CurrentInUse;
+ if (s_CurrentInUse > s_PeakInUseShort)
+ s_PeakInUseShort = s_CurrentInUse;
+ if (s_CurrentInUse > s_PeakInUseLong)
+ s_PeakInUseLong = s_CurrentInUse;
if (s_Count > 0)
{
+ s_ConsecutiveMiss = 0;
int idx = --s_Count;
T item = s_Stack[idx];
s_Stack[idx] = null;
@@ -90,6 +99,7 @@ namespace AlicizaX
return item;
}
+ s_ConsecutiveMiss++;
return CreateNew();
}
@@ -105,107 +115,139 @@ namespace AlicizaX
{
if (item == null) return;
+ MemoryPoolRegistry.ScheduleTick(s_Handle);
EnsureStrictCheckState();
if (MemoryPool.EnableStrictCheck && s_InPoolSet.ContainsKey(item))
throw new InvalidOperationException($"MemoryPool<{typeof(T).Name}>: Double release detected.");
s_ReleaseCount++;
+ s_ReleaseThisFrame++;
if (s_CurrentInUse > 0)
s_CurrentInUse--;
item.Clear();
- if (s_Count >= s_MaxCapacity)
+ if (s_Count >= s_HardCapacity)
return;
- if (s_Count == s_Stack.Length)
- Grow();
-
+ EnsureStackCapacity(s_Count + 1);
AddToStrictCheckSet(item);
s_Stack[s_Count++] = item;
}
- internal static void Tick(int frameCount)
+ internal static bool Tick(int frameCount)
{
- if (frameCount == s_LastTickFrame) return;
+ if (frameCount == s_LastTickFrame) return true;
s_LastTickFrame = frameCount;
- if (s_PeakInUse > s_HighWaterMark)
- s_HighWaterMark = s_PeakInUse;
-
- if (s_RecentAcquireCount == 0)
- s_IdleFrames++;
- else
- s_IdleFrames = 0;
-
- s_RecentAcquireCount = 0;
-
- if (s_Count <= MIN_KEEP) return;
-
- if (s_IdleFrames >= IDLE_THRESHOLD)
+ bool active = s_AcquireThisFrame > 0 || s_ReleaseThisFrame > 0 || s_CurrentInUse > 0;
+ if (active)
{
- int target = Math.Max((int)(s_HighWaterMark * 1.5f), MIN_KEEP);
+ s_HotFrames++;
+ s_IdleFrames = 0;
+ }
+ else
+ {
+ s_IdleFrames++;
+ s_HotFrames = 0;
+ }
- if (s_Count > target)
- {
- int excess = s_Count - target;
+ UpdateTargetReserve();
+ FillReserveBudgeted();
+ ReleaseExcessBudgeted();
- float ratio = s_IdleFrames < IDLE_AGGRESSIVE ? 0.25f : 0.5f;
- int removeCount = Math.Max((int)(excess * ratio), 1);
+ s_AcquireThisFrame = 0;
+ s_ReleaseThisFrame = 0;
- int newCount = s_Count - removeCount;
- RemoveRangeFromStrictCheckSet(newCount, removeCount);
- Array.Clear(s_Stack, newCount, removeCount);
- s_Count = newCount;
+ return s_IdleFrames < UNSCHEDULE_IDLE_FRAMES || s_Count > s_TargetReserve;
+ }
- TryShrinkArray();
- }
+ private static void UpdateTargetReserve()
+ {
+ if (s_IdleFrames >= SHORT_DECAY_START && s_PeakInUseShort > 0)
+ s_PeakInUseShort -= Math.Max(1, s_PeakInUseShort >> 4);
- if (s_IdleFrames >= IDLE_AGGRESSIVE)
- {
- s_HighWaterMark = Math.Max(s_HighWaterMark >> 1, MIN_KEEP);
- s_PeakInUse = s_CurrentInUse;
- }
+ if (s_IdleFrames >= LONG_DECAY_START && s_PeakInUseLong > 0)
+ s_PeakInUseLong -= Math.Max(1, s_PeakInUseLong >> 6);
+
+ int shortReserve = s_PeakInUseShort + (s_PeakInUseShort >> 1);
+ int longReserve = s_PeakInUseLong + (s_PeakInUseLong >> 2);
+ int desired = Math.Max(shortReserve, longReserve);
+ if (desired < MIN_KEEP) desired = MIN_KEEP;
+ if (desired > s_SoftCapacity) desired = s_SoftCapacity;
+ s_TargetReserve = desired;
+ }
+
+ private static void FillReserveBudgeted()
+ {
+ int available = s_Count + s_CurrentInUse;
+ if (available >= s_TargetReserve || s_Count >= s_SoftCapacity)
+ return;
+
+ int need = s_TargetReserve - available;
+ int budget = GetCreateBudget();
+ int createCount = Math.Min(need, budget);
+ int room = s_SoftCapacity - s_Count;
+ if (createCount > room) createCount = room;
+
+ for (int i = 0; i < createCount; i++)
+ {
+ EnsureStackCapacity(s_Count + 1);
+ T item = new T();
+ s_CreateCount++;
+ s_Stack[s_Count++] = item;
+ AddToStrictCheckSet(item);
}
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetCreateBudget()
+ {
+ if (s_ConsecutiveMiss > 0) return 8;
+ if (s_HotFrames > 0) return 4;
+ return 1;
+ }
+
+ private static void ReleaseExcessBudgeted()
+ {
+ if (s_IdleFrames < SHORT_DECAY_START || s_Count <= s_TargetReserve)
+ return;
+
+ int excess = s_Count - s_TargetReserve;
+ int budget = s_IdleFrames < LONG_DECAY_START ? 4 : 16;
+ int removeCount = Math.Min(excess, budget);
+ int newCount = s_Count - removeCount;
+ RemoveRangeFromStrictCheckSet(newCount, removeCount);
+ Array.Clear(s_Stack, newCount, removeCount);
+ s_Count = newCount;
+ }
[MethodImpl(MethodImplOptions.NoInlining)]
- private static void Grow()
+ private static void EnsureStackCapacity(int required)
{
- int newLen = s_Stack.Length == 0 ? 8 : s_Stack.Length << 1;
- if (newLen > s_MaxCapacity) newLen = s_MaxCapacity;
+ if (s_Stack.Length >= required)
+ return;
+
+ int newLen = s_Stack.Length == 0 ? 8 : s_Stack.Length;
+ while (newLen < required)
+ newLen <<= 1;
+ if (newLen > s_HardCapacity)
+ newLen = s_HardCapacity;
+
var newStack = new T[newLen];
Array.Copy(s_Stack, 0, newStack, 0, s_Count);
s_Stack = newStack;
}
- [MethodImpl(MethodImplOptions.NoInlining)]
- private static void TryShrinkArray()
- {
- if (s_Stack.Length > 32 && s_Count < s_Stack.Length >> 2)
- {
- int newLen = Math.Max(s_Count << 1, 8);
- var newStack = new T[newLen];
- Array.Copy(s_Stack, 0, newStack, 0, s_Count);
- s_Stack = newStack;
- }
- }
-
-
public static void Prewarm(int count)
{
- count = Math.Min(count, s_MaxCapacity);
+ MemoryPoolRegistry.ScheduleTick(s_Handle);
+ count = Math.Min(count, s_HardCapacity);
if (count <= s_Count) return;
- if (count > s_Stack.Length)
- {
- var newStack = new T[count];
- Array.Copy(s_Stack, 0, newStack, 0, s_Count);
- s_Stack = newStack;
- }
+ EnsureStackCapacity(count);
while (s_Count < count)
{
@@ -214,6 +256,9 @@ namespace AlicizaX
AddToStrictCheckSet(item);
s_CreateCount++;
}
+
+ if (count > s_TargetReserve)
+ s_TargetReserve = Math.Min(count, s_SoftCapacity);
}
public static void Shrink(int keepCount)
@@ -224,23 +269,57 @@ namespace AlicizaX
RemoveRangeFromStrictCheckSet(keepCount, s_Count - keepCount);
Array.Clear(s_Stack, keepCount, s_Count - keepCount);
s_Count = keepCount;
- TryShrinkArray();
+ if (s_TargetReserve > keepCount)
+ s_TargetReserve = Math.Max(keepCount, MIN_KEEP);
+ }
+
+ public static void Compact()
+ {
+ int newLen = s_Count <= 0 ? 0 : Math.Max(NextPowerOfTwo(s_Count), MIN_KEEP);
+ if (newLen == s_Stack.Length)
+ return;
+
+ if (newLen == 0)
+ {
+ s_Stack = Array.Empty();
+ return;
+ }
+
+ var newStack = new T[newLen];
+ Array.Copy(s_Stack, 0, newStack, 0, s_Count);
+ s_Stack = newStack;
}
public static void SetMaxCapacity(int max)
{
- s_MaxCapacity = Math.Max(max, MIN_KEEP);
+ SetCapacity(max, Math.Max(max << 2, MIN_KEEP));
+ }
+
+ public static void SetCapacity(int softCapacity, int hardCapacity)
+ {
+ softCapacity = Math.Max(softCapacity, MIN_KEEP);
+ hardCapacity = Math.Max(hardCapacity, softCapacity);
+ s_SoftCapacity = softCapacity;
+ s_HardCapacity = hardCapacity;
+ if (s_TargetReserve > s_SoftCapacity)
+ s_TargetReserve = s_SoftCapacity;
}
public static void ClearAll()
{
+ MemoryPoolRegistry.UnscheduleTick(s_Handle);
ResetStrictCheckSet();
Array.Clear(s_Stack, 0, s_Count);
s_Count = 0;
- s_HighWaterMark = s_CurrentInUse;
- s_PeakInUse = s_CurrentInUse;
+ s_CurrentInUse = 0;
+ s_PeakInUseShort = 0;
+ s_PeakInUseLong = 0;
+ s_AcquireThisFrame = 0;
+ s_ReleaseThisFrame = 0;
+ s_TargetReserve = MIN_KEEP;
s_IdleFrames = 0;
- s_RecentAcquireCount = 0;
+ s_HotFrames = 0;
+ s_ConsecutiveMiss = 0;
s_Stack = Array.Empty();
}
@@ -257,7 +336,7 @@ namespace AlicizaX
s_CurrentInUse,
s_AcquireCount, s_ReleaseCount,
s_CreateCount,
- s_HighWaterMark, s_MaxCapacity,
+ s_TargetReserve, s_SoftCapacity,
s_IdleFrames, s_Stack.Length);
}
@@ -338,5 +417,16 @@ namespace AlicizaX
s_InPoolSet.Clear();
}
+
+ private static int NextPowerOfTwo(int value)
+ {
+ value--;
+ value |= value >> 1;
+ value |= value >> 2;
+ value |= value >> 4;
+ value |= value >> 8;
+ value |= value >> 16;
+ return value + 1;
+ }
}
}
diff --git a/Runtime/MemoryPool/MemoryPool.cs b/Runtime/MemoryPool/MemoryPool.cs
index c703de2..8ae971e 100644
--- a/Runtime/MemoryPool/MemoryPool.cs
+++ b/Runtime/MemoryPool/MemoryPool.cs
@@ -1,18 +1,14 @@
using System;
+using System.Runtime.CompilerServices;
namespace AlicizaX
{
- ///
- /// 内存池。保留旧 API 签名,内部转发到 MemoryPool<T> / MemoryPoolRegistry。
- ///
public static partial class MemoryPool
{
private static bool _enableStrictCheck;
private static int _strictCheckVersion;
- ///
- /// 获取或设置是否开启强制检查。
- ///
+
public static bool EnableStrictCheck
{
get => _enableStrictCheck;
@@ -28,100 +24,100 @@ namespace AlicizaX
internal static int StrictCheckVersion => _strictCheckVersion;
- ///
- /// 获取内存池的数量。
- ///
+
public static int Count => MemoryPoolRegistry.Count;
- ///
- /// 获取所有内存池的信息。
- ///
+
+#if UNITY_EDITOR
public static MemoryPoolInfo[] GetAllMemoryPoolInfos()
{
return MemoryPoolRegistry.GetAllInfos();
}
+#endif
public static int GetAllMemoryPoolInfos(MemoryPoolInfo[] infos)
{
return MemoryPoolRegistry.GetAllInfos(infos);
}
- ///
- /// 清除所有内存池。
- ///
+
public static void ClearAll()
{
MemoryPoolRegistry.ClearAll();
}
- ///
- /// 从内存池获取内存对象。
- ///
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T Acquire() where T : class, IMemory, new()
{
return MemoryPool.Acquire();
}
- ///
- /// 从内存池获取内存对象。
- ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IMemory Acquire(Type memoryType)
{
return MemoryPoolRegistry.Acquire(memoryType);
}
- ///
- /// 将内存对象归还内存池。
- ///
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Release(T memory) where T : class, IMemory, new()
+ {
+ MemoryPool.Release(memory);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Release(IMemory memory)
{
MemoryPoolRegistry.Release(memory);
}
- ///
- /// 向内存池中预热指定数量的内存对象。
- ///
public static void Add(int count) where T : class, IMemory, new()
{
MemoryPool.Prewarm(count);
}
- ///
- /// 向内存池中预热指定数量的内存对象。
- ///
public static void Add(Type memoryType, int count)
{
MemoryPoolRegistry.Prewarm(memoryType, count);
}
- ///
- /// 从内存池中移除指定数量的内存对象。
- ///
public static void Remove(int count) where T : class, IMemory, new()
{
int target = MemoryPool.UnusedCount - count;
MemoryPool.Shrink(target);
}
- ///
- /// 从内存池中移除指定数量的内存对象。
- ///
public static void Remove(Type memoryType, int count)
{
MemoryPoolRegistry.RemoveFromType(memoryType, count);
}
- ///
- /// 从内存池中移除所有的内存对象。
- ///
public static void RemoveAll() where T : class, IMemory, new()
{
MemoryPool.ClearAll();
}
- ///
- /// 从内存池中移除所有的内存对象。
- ///
+ public static void SetCapacity(int softCapacity, int hardCapacity) where T : class, IMemory, new()
+ {
+ MemoryPool.SetCapacity(softCapacity, hardCapacity);
+ }
+
+ public static void Compact() where T : class, IMemory, new()
+ {
+ MemoryPool.Compact();
+ }
+
+ public static void Compact(Type memoryType)
+ {
+ MemoryPoolRegistry.CompactType(memoryType);
+ }
+
+ public static void CompactAll()
+ {
+ MemoryPoolRegistry.CompactAll();
+ }
+
public static void RemoveAll(Type memoryType)
{
MemoryPoolRegistry.ClearType(memoryType);
diff --git a/Runtime/MemoryPool/MemoryPoolRegistry.cs b/Runtime/MemoryPool/MemoryPoolRegistry.cs
index e08fe43..f3c092a 100644
--- a/Runtime/MemoryPool/MemoryPoolRegistry.cs
+++ b/Runtime/MemoryPool/MemoryPoolRegistry.cs
@@ -4,7 +4,6 @@ using System.Runtime.CompilerServices;
namespace AlicizaX
{
-
public static class MemoryPoolRegistry
{
internal sealed class MemoryPoolHandle
@@ -13,6 +12,7 @@ namespace AlicizaX
public delegate void ReleaseHandler(IMemory memory);
public delegate void ClearHandler();
public delegate void IntHandler(int value);
+ public delegate bool TickHandler(int value);
public delegate void GetInfoHandler(ref MemoryPoolInfo info);
public readonly AcquireHandler Acquire;
@@ -20,8 +20,10 @@ namespace AlicizaX
public readonly ClearHandler Clear;
public readonly IntHandler Prewarm;
public readonly GetInfoHandler GetInfo;
- public readonly IntHandler Tick;
+ public readonly TickHandler Tick;
public readonly IntHandler Shrink;
+ public readonly ClearHandler Compact;
+ public int ActiveIndex = -1;
public MemoryPoolHandle(
AcquireHandler acquire,
@@ -29,8 +31,9 @@ namespace AlicizaX
ClearHandler clear,
IntHandler prewarm,
GetInfoHandler getInfo,
- IntHandler tick,
- IntHandler shrink)
+ TickHandler tick,
+ IntHandler shrink,
+ ClearHandler compact)
{
Acquire = acquire;
Release = release;
@@ -39,27 +42,65 @@ namespace AlicizaX
GetInfo = getInfo;
Tick = tick;
Shrink = shrink;
+ Compact = compact;
}
}
private static readonly Dictionary s_Handles
= new Dictionary(64);
- private static MemoryPoolHandle.IntHandler[] s_TickArray = Array.Empty();
- private static int s_TickCount;
- private static bool s_TickArrayDirty;
+ private static MemoryPoolHandle[] s_ActivePools = Array.Empty();
+ private static int s_ActiveCount;
public static int Count => s_Handles.Count;
internal static void Register(Type type, MemoryPoolHandle handle)
{
s_Handles[type] = handle;
- s_TickArrayDirty = true;
}
- ///
- /// 非泛型 Acquire,用于只有 Type 没有泛型参数的场景。
- ///
+ internal static void ScheduleTick(MemoryPoolHandle handle)
+ {
+ if (handle == null || handle.ActiveIndex >= 0)
+ return;
+
+ if (s_ActiveCount == s_ActivePools.Length)
+ {
+ int newLength = s_ActivePools.Length == 0 ? 16 : s_ActivePools.Length << 1;
+ var activePools = new MemoryPoolHandle[newLength];
+ Array.Copy(s_ActivePools, 0, activePools, 0, s_ActiveCount);
+ s_ActivePools = activePools;
+ }
+
+ handle.ActiveIndex = s_ActiveCount;
+ s_ActivePools[s_ActiveCount++] = handle;
+ }
+
+
+ internal static void UnscheduleTick(MemoryPoolHandle handle)
+ {
+ if (handle == null)
+ return;
+
+ int index = handle.ActiveIndex;
+ if (index < 0 || index >= s_ActiveCount)
+ {
+ handle.ActiveIndex = -1;
+ return;
+ }
+
+ int lastIndex = --s_ActiveCount;
+ MemoryPoolHandle last = s_ActivePools[lastIndex];
+ s_ActivePools[lastIndex] = null;
+ handle.ActiveIndex = -1;
+
+ if (index != lastIndex)
+ {
+ s_ActivePools[index] = last;
+ last.ActiveIndex = index;
+ }
+ }
+
public static IMemory Acquire(Type type)
{
if (type == null)
@@ -129,6 +170,18 @@ namespace AlicizaX
{
foreach (var kv in s_Handles)
kv.Value.Clear();
+
+ for (int i = 0; i < s_ActiveCount; i++)
+ s_ActivePools[i].ActiveIndex = -1;
+
+ Array.Clear(s_ActivePools, 0, s_ActiveCount);
+ s_ActiveCount = 0;
+ }
+
+ public static void CompactAll()
+ {
+ foreach (var kv in s_Handles)
+ kv.Value.Compact();
}
public static void Prewarm(Type type, int count)
@@ -168,6 +221,21 @@ namespace AlicizaX
handle.Clear();
}
+ public static void CompactType(Type type)
+ {
+ if (type == null)
+ throw new ArgumentNullException(nameof(type));
+
+ if (!s_Handles.TryGetValue(type, out var handle))
+ {
+ EnsureRegistered(type);
+ if (!s_Handles.TryGetValue(type, out handle))
+ throw new Exception($"MemoryPool: Type '{type.FullName}' is not a valid IMemory type.");
+ }
+
+ handle.Compact();
+ }
+
public static void RemoveFromType(Type type, int count)
{
if (type == null)
@@ -195,27 +263,29 @@ namespace AlicizaX
throw new Exception($"MemoryPool: Type '{type.FullName}' is not a valid IMemory type.");
}
-
public static void TickAll(int frameCount)
{
- if (s_TickArrayDirty)
- RebuildTickArray();
-
- for (int i = 0; i < s_TickCount; i++)
- s_TickArray[i](frameCount);
- }
-
- private static void RebuildTickArray()
- {
- s_TickCount = s_Handles.Count;
- if (s_TickArray.Length < s_TickCount)
- s_TickArray = new MemoryPoolHandle.IntHandler[s_TickCount];
-
int i = 0;
- foreach (var kv in s_Handles)
- s_TickArray[i++] = kv.Value.Tick;
+ while (i < s_ActiveCount)
+ {
+ MemoryPoolHandle handle = s_ActivePools[i];
+ if (handle.Tick(frameCount))
+ {
+ i++;
+ continue;
+ }
- s_TickArrayDirty = false;
+ int lastIndex = --s_ActiveCount;
+ MemoryPoolHandle last = s_ActivePools[lastIndex];
+ s_ActivePools[lastIndex] = null;
+ handle.ActiveIndex = -1;
+
+ if (i != lastIndex)
+ {
+ s_ActivePools[i] = last;
+ last.ActiveIndex = i;
+ }
+ }
}
private static void EnsureRegistered(Type type)
diff --git a/Runtime/ObjectPool/Benchmark.meta b/Runtime/ObjectPool/Benchmark.meta
new file mode 100644
index 0000000..bc6d7e9
--- /dev/null
+++ b/Runtime/ObjectPool/Benchmark.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b0a235d2743b4ec68a4c0bfd5dc2eae6
+timeCreated: 1777269624
\ No newline at end of file
diff --git a/Runtime/ObjectPool/Benchmark/ObjectPoolBenchmark.cs b/Runtime/ObjectPool/Benchmark/ObjectPoolBenchmark.cs
new file mode 100644
index 0000000..cb7fdf3
--- /dev/null
+++ b/Runtime/ObjectPool/Benchmark/ObjectPoolBenchmark.cs
@@ -0,0 +1,739 @@
+#if UNITY_EDITOR
+using System;
+using System.Diagnostics;
+using Cysharp.Text;
+using Unity.Profiling;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+
+namespace AlicizaX.ObjectPool
+{
+ [DisallowMultipleComponent]
+ [AddComponentMenu("Game Framework/ObjectPool Benchmark")]
+ public sealed class ObjectPoolBenchmark : MonoBehaviour
+ {
+ [SerializeField] private bool runOnStart = true;
+ [SerializeField] private int objectCount = 10000;
+ [SerializeField] private int loopCount = 100000;
+ [SerializeField] private int sameNameCount = 1024;
+ [SerializeField] private int occupiedSameNameCount = 1023;
+ [SerializeField] private int extremeSameNameCount = 4096;
+ [SerializeField] private bool includeReleaseAllUnused = true;
+ [SerializeField] private bool logEachCase = true;
+ [SerializeField] private bool logMemoryDelta = true;
+ [SerializeField] private int maxCapturedLogChars = 128 * 1024;
+
+ private static readonly ProfilerMarker s_TotalMarker = new ProfilerMarker("ObjectPoolBenchmark.Total");
+ private static readonly ProfilerMarker s_RegisterMarker = new ProfilerMarker("ObjectPoolBenchmark.Register");
+ private static readonly ProfilerMarker s_SpawnUnspawnMarker = new ProfilerMarker("ObjectPoolBenchmark.SpawnUnspawn");
+ private static readonly ProfilerMarker s_OccupiedSameNameMarker = new ProfilerMarker("ObjectPoolBenchmark.OccupiedSameName");
+ private static readonly ProfilerMarker s_ExtremeSameNameMarker = new ProfilerMarker("ObjectPoolBenchmark.ExtremeSameName");
+ private static readonly ProfilerMarker s_MultiSpawnMarker = new ProfilerMarker("ObjectPoolBenchmark.MultiSpawn");
+ private static readonly ProfilerMarker s_CapacityMarker = new ProfilerMarker("ObjectPoolBenchmark.Capacity");
+ private static readonly ProfilerMarker s_ExpireMarker = new ProfilerMarker("ObjectPoolBenchmark.Expire");
+ private static readonly ProfilerMarker s_ReleaseMarker = new ProfilerMarker("ObjectPoolBenchmark.ReleaseAllUnused");
+ private static readonly ProfilerMarker s_MixedNameMarker = new ProfilerMarker("ObjectPoolBenchmark.MixedName");
+ private static readonly ProfilerMarker s_DestroyMarker = new ProfilerMarker("ObjectPoolBenchmark.DestroyRecreate");
+ private static readonly ProfilerMarker s_CursorRecoveryMarker = new ProfilerMarker("ObjectPoolBenchmark.CursorRecovery");
+
+ private readonly Stopwatch m_Stopwatch = new Stopwatch();
+ private Utf16ValueStringBuilder m_LogBuilder;
+ private IObjectPoolService m_Service;
+ private int m_FailCount;
+ private int m_CaseCount;
+ private bool m_LogBuilderCreated;
+ private long m_CaseAllocBefore;
+ private long m_CaseAllocAfter;
+
+ private void OnEnable()
+ {
+ ClearCapturedConsoleOutput();
+ Application.logMessageReceived += OnLogMessageReceived;
+ }
+
+ private void OnDisable()
+ {
+ Application.logMessageReceived -= OnLogMessageReceived;
+ m_LogBuilder.Dispose();
+ m_LogBuilderCreated = false;
+ }
+
+ private void Start()
+ {
+ if (runOnStart)
+ RunAll();
+ }
+
+ [ContextMenu("Run ObjectPool Benchmark")]
+ public void RunAll()
+ {
+ ClearCapturedConsoleOutput();
+ EnsureService();
+ if (m_Service == null)
+ return;
+
+ m_FailCount = 0;
+ m_CaseCount = 0;
+
+ using (s_TotalMarker.Auto())
+ {
+ RunCase("SingleName Spawn/Unspawn", RunSingleNameSpawnUnspawn);
+ RunCase("NullName Normalization", RunNullNameNormalization);
+ RunCase("SameName Stress", RunSameNameStress);
+ RunCase("SameName Occupied Scan", RunSameNameOccupiedScan);
+ RunCase("SameName Extreme OneFree", RunSameNameExtremeOneFree);
+ RunCase("MultiSpawn", RunMultiSpawn);
+ RunCase("MixedName RoundRobin", RunMixedNameRoundRobin);
+ RunCase("Capacity Guard", RunCapacityGuard);
+ RunCase("Spawned Release Guard", RunSpawnedReleaseGuard);
+ RunCase("Destroy Recreate", RunDestroyRecreate);
+ RunCase("Cursor Release Recovery", RunCursorReleaseRecovery);
+ RunCase("Locked Release Guard", RunLockedReleaseGuard);
+ RunCase("Custom Release Guard", RunCustomReleaseGuard);
+ RunCase("Expire Release", RunExpireRelease);
+
+ if (includeReleaseAllUnused)
+ RunCase("ReleaseAllUnused", RunReleaseAllUnused);
+ }
+
+ Debug.Log(BuildLog("ObjectPool benchmark finished. cases=", m_CaseCount, ", fails=", m_FailCount));
+ }
+
+ [ContextMenu("Copy Captured Console Output")]
+ public void CopyCapturedConsoleOutput()
+ {
+ EnsureLogBuilder();
+ string text = m_LogBuilder.ToString();
+ GUIUtility.systemCopyBuffer = text;
+ Debug.Log(BuildLog("ObjectPoolBenchmark copied console output chars=", text.Length, ", max=", maxCapturedLogChars));
+ }
+
+ [ContextMenu("Clear Captured Console Output")]
+ public void ClearCapturedConsoleOutput()
+ {
+ m_LogBuilder.Dispose();
+ m_LogBuilder = ZString.CreateStringBuilder();
+ m_LogBuilderCreated = true;
+ }
+
+ private void EnsureLogBuilder()
+ {
+ if (!m_LogBuilderCreated)
+ {
+ m_LogBuilder = ZString.CreateStringBuilder();
+ m_LogBuilderCreated = true;
+ }
+ }
+
+ private void OnLogMessageReceived(string condition, string stackTrace, LogType type)
+ {
+ EnsureLogBuilder();
+ if (m_LogBuilder.Length >= maxCapturedLogChars)
+ return;
+
+ m_LogBuilder.Append('[');
+ m_LogBuilder.Append(type);
+ m_LogBuilder.Append("] ");
+ m_LogBuilder.Append(condition);
+ m_LogBuilder.AppendLine();
+
+ if (type == LogType.Exception || type == LogType.Error || type == LogType.Assert)
+ {
+ m_LogBuilder.Append(stackTrace);
+ m_LogBuilder.AppendLine();
+ }
+ }
+
+ private void EnsureService()
+ {
+ if (m_Service != null)
+ return;
+
+ if (AppServices.TryGet(out m_Service))
+ return;
+
+ if (!AppServices.HasWorld && GetComponent() == null)
+ gameObject.AddComponent();
+
+ if (GetComponent() == null)
+ gameObject.AddComponent();
+
+ if (!AppServices.TryGet(out m_Service))
+ Debug.LogError("ObjectPoolBenchmark requires ObjectPoolComponent registration.");
+ }
+
+ private void RunCase(string caseName, Action action)
+ {
+ m_CaseCount++;
+ m_CaseAllocBefore = GetAllocatedBytesForCurrentThread();
+ m_CaseAllocAfter = m_CaseAllocBefore;
+ m_Stopwatch.Restart();
+ action();
+ if (m_Stopwatch.IsRunning)
+ {
+ m_Stopwatch.Stop();
+ m_CaseAllocAfter = GetAllocatedBytesForCurrentThread();
+ }
+
+ if (logEachCase)
+ {
+ if (logMemoryDelta)
+ Debug.Log(BuildLog("[ObjectPoolBenchmark] ", caseName, " ms=", m_Stopwatch.Elapsed.TotalMilliseconds, " gcAlloc=", m_CaseAllocAfter - m_CaseAllocBefore));
+ else
+ Debug.Log(BuildLog("[ObjectPoolBenchmark] ", caseName, " ms=", m_Stopwatch.Elapsed.TotalMilliseconds));
+ }
+ }
+
+ private void RestartCaseMeasure()
+ {
+ m_CaseAllocBefore = GetAllocatedBytesForCurrentThread();
+ m_CaseAllocAfter = m_CaseAllocBefore;
+ m_Stopwatch.Restart();
+ }
+
+ private void StopCaseMeasure()
+ {
+ m_Stopwatch.Stop();
+ m_CaseAllocAfter = GetAllocatedBytesForCurrentThread();
+ }
+
+ private long GetAllocatedBytesForCurrentThread()
+ {
+ return logMemoryDelta ? GC.GetAllocatedBytesForCurrentThread() : 0L;
+ }
+
+ private void RunSingleNameSpawnUnspawn()
+ {
+ using (s_SpawnUnspawnMarker.Auto())
+ {
+ string poolName = MakePoolName("single");
+ IObjectPool pool = CreatePool(poolName, false, objectCount, float.MaxValue);
+ BenchmarkTarget target = new BenchmarkTarget(1);
+ BenchmarkObject obj = BenchmarkObject.Create(string.Empty, target, false, true);
+ pool.Register(obj, false);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn();
+ AssertReference(spawned, obj, "single spawn returned wrong object");
+ pool.Unspawn(target);
+ }
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunNullNameNormalization()
+ {
+ string poolName = MakePoolName("null-name");
+ IObjectPool pool = CreatePool(poolName, false, 4, float.MaxValue);
+ BenchmarkTarget target = new BenchmarkTarget(2);
+ BenchmarkObject obj = BenchmarkObject.Create(null, target, false, true);
+ pool.Register(obj, false);
+
+ RestartCaseMeasure();
+ BenchmarkObject spawned = pool.Spawn(null);
+ AssertReference(spawned, obj, "Spawn(null) failed for empty-name object");
+ pool.Unspawn(target);
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+
+ private void RunSameNameStress()
+ {
+ using (s_RegisterMarker.Auto())
+ {
+ string poolName = MakePoolName("same-name");
+ IObjectPool pool = CreatePool(poolName, false, sameNameCount, float.MaxValue);
+ BenchmarkTarget[] targets = new BenchmarkTarget[sameNameCount];
+ BenchmarkObject[] objects = new BenchmarkObject[sameNameCount];
+
+ for (int i = 0; i < sameNameCount; i++)
+ {
+ targets[i] = new BenchmarkTarget(i);
+ objects[i] = BenchmarkObject.Create("same", targets[i], false, true);
+ pool.Register(objects[i], false);
+ }
+
+ int cycles = Math.Max(1, loopCount / Math.Max(1, sameNameCount));
+ RestartCaseMeasure();
+ for (int c = 0; c < cycles; c++)
+ {
+ for (int i = 0; i < sameNameCount; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("same");
+ AssertNotNull(spawned, "same-name spawn returned null");
+ pool.Unspawn(spawned.Target);
+ }
+ }
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunSameNameOccupiedScan()
+ {
+ using (s_OccupiedSameNameMarker.Auto())
+ {
+ int totalCount = Math.Max(2, sameNameCount);
+ int blockedCount = Math.Max(1, Math.Min(occupiedSameNameCount, totalCount - 1));
+ int cycles = Math.Max(1, loopCount / Math.Max(1, blockedCount));
+ string poolName = MakePoolName("same-name-occupied");
+ IObjectPool pool = CreatePool(poolName, false, totalCount, float.MaxValue);
+ BenchmarkTarget[] blockedTargets = new BenchmarkTarget[blockedCount];
+
+ for (int i = 0; i < totalCount; i++)
+ pool.Register(BenchmarkObject.Create("occupied", new BenchmarkTarget(i), false, true), false);
+
+ for (int i = 0; i < blockedCount; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("occupied");
+ AssertNotNull(spawned, "occupied setup spawn returned null");
+ if (spawned != null)
+ blockedTargets[i] = spawned.Target;
+ }
+
+ RestartCaseMeasure();
+ for (int i = 0; i < cycles; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("occupied");
+ AssertNotNull(spawned, "occupied scan spawn returned null");
+ if (spawned != null)
+ pool.Unspawn(spawned.Target);
+ }
+ StopCaseMeasure();
+
+ for (int i = 0; i < blockedCount; i++)
+ {
+ if (blockedTargets[i] != null)
+ pool.Unspawn(blockedTargets[i]);
+ }
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunSameNameExtremeOneFree()
+ {
+ using (s_ExtremeSameNameMarker.Auto())
+ {
+ int totalCount = Math.Max(2, extremeSameNameCount);
+ int blockedCount = totalCount - 1;
+ string poolName = MakePoolName("same-name-extreme");
+ IObjectPool pool = CreatePool(poolName, false, totalCount, float.MaxValue);
+ BenchmarkTarget[] blockedTargets = new BenchmarkTarget[blockedCount];
+
+ for (int i = 0; i < totalCount; i++)
+ pool.Register(BenchmarkObject.Create("extreme", new BenchmarkTarget(i), false, true), false);
+
+ for (int i = 0; i < blockedCount; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("extreme");
+ AssertNotNull(spawned, "extreme setup spawn returned null");
+ if (spawned != null)
+ blockedTargets[i] = spawned.Target;
+ }
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("extreme");
+ AssertNotNull(spawned, "extreme one-free spawn returned null");
+ if (spawned != null)
+ pool.Unspawn(spawned.Target);
+ }
+ StopCaseMeasure();
+
+ for (int i = 0; i < blockedCount; i++)
+ {
+ if (blockedTargets[i] != null)
+ pool.Unspawn(blockedTargets[i]);
+ }
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunMultiSpawn()
+ {
+ using (s_MultiSpawnMarker.Auto())
+ {
+ string poolName = MakePoolName("multi");
+ IObjectPool pool = CreatePool(poolName, true, 4, float.MaxValue);
+ BenchmarkTarget target = new BenchmarkTarget(3);
+ BenchmarkObject obj = BenchmarkObject.Create("multi", target, false, true);
+ pool.Register(obj, false);
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("multi");
+ AssertReference(spawned, obj, "multi spawn returned wrong object");
+ }
+
+ for (int i = 0; i < loopCount; i++)
+ pool.Unspawn(target);
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunMixedNameRoundRobin()
+ {
+ using (s_MixedNameMarker.Auto())
+ {
+ const int nameCount = 16;
+ int perNameCount = Math.Max(1, sameNameCount / nameCount);
+ string poolName = MakePoolName("mixed-name");
+ IObjectPool pool = CreatePool(poolName, false, sameNameCount, float.MaxValue);
+ string[] names = new string[nameCount];
+
+ for (int n = 0; n < nameCount; n++)
+ {
+ names[n] = MakeIndexedName("mixed", n);
+ for (int i = 0; i < perNameCount; i++)
+ pool.Register(BenchmarkObject.Create(names[n], new BenchmarkTarget(n * perNameCount + i), false, true), false);
+ }
+
+ RestartCaseMeasure();
+ for (int i = 0; i < loopCount; i++)
+ {
+ string objectName = names[i & (nameCount - 1)];
+ BenchmarkObject spawned = pool.Spawn(objectName);
+ AssertNotNull(spawned, "mixed-name spawn returned null");
+ if (spawned != null)
+ pool.Unspawn(spawned.Target);
+ }
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunCapacityGuard()
+ {
+ using (s_CapacityMarker.Auto())
+ {
+ string poolName = MakePoolName("capacity");
+ IObjectPool pool = CreatePool(poolName, false, 1, float.MaxValue);
+ BenchmarkTarget first = new BenchmarkTarget(4);
+ BenchmarkTarget second = new BenchmarkTarget(5);
+ BenchmarkObject firstObject = BenchmarkObject.Create("cap", first, false, true);
+ BenchmarkObject secondObject = BenchmarkObject.Create("cap", second, false, true);
+
+ RestartCaseMeasure();
+ pool.Register(firstObject, false);
+ pool.Register(secondObject, false);
+
+ AssertEqual(pool.Count, 1, "capacity guard allowed over-register");
+ BenchmarkObject spawned = pool.Spawn("cap");
+ AssertNotNull(spawned, "capacity replacement object cannot spawn");
+ if (spawned != null)
+ AssertReference(spawned.Target, second, "capacity replacement kept wrong object");
+
+ pool.Unspawn(second);
+ pool.ReleaseAllUnused();
+ AssertEqual(pool.Count, 0, "capacity release did not clear unused object");
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunSpawnedReleaseGuard()
+ {
+ string poolName = MakePoolName("spawned-release");
+ IObjectPool pool = CreatePool(poolName, false, 4, float.MaxValue);
+ BenchmarkTarget target = new BenchmarkTarget(8);
+ pool.Register(BenchmarkObject.Create("spawned", target, false, true), true);
+
+ RestartCaseMeasure();
+ pool.ReleaseAllUnused();
+ AssertEqual(pool.Count, 1, "spawned object was released while in use");
+ pool.Unspawn(target);
+ pool.ReleaseAllUnused();
+ AssertEqual(pool.Count, 0, "spawned object was not released after unspawn");
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+
+ private void RunDestroyRecreate()
+ {
+ using (s_DestroyMarker.Auto())
+ {
+ string poolName = MakePoolName("destroy-recreate");
+
+ RestartCaseMeasure();
+ for (int i = 0; i < 64; i++)
+ {
+ IObjectPool pool = CreatePool(poolName, false, 8, float.MaxValue);
+ pool.Register(BenchmarkObject.Create("destroy", new BenchmarkTarget(i), false, true), false);
+ AssertEqual(pool.Count, 1, "destroy-recreate register failed");
+ DestroyPool(poolName);
+ }
+ StopCaseMeasure();
+ }
+ }
+
+ private void RunCursorReleaseRecovery()
+ {
+ using (s_CursorRecoveryMarker.Auto())
+ {
+ string poolName = MakePoolName("cursor-recovery");
+ IObjectPool pool = CreatePool(poolName, false, 4, float.MaxValue);
+ BenchmarkTarget[] blockedTargets = new BenchmarkTarget[3];
+
+ for (int i = 0; i < 4; i++)
+ pool.Register(BenchmarkObject.Create("cursor", new BenchmarkTarget(i), false, true), false);
+
+ for (int i = 0; i < blockedTargets.Length; i++)
+ {
+ BenchmarkObject spawned = pool.Spawn("cursor");
+ AssertNotNull(spawned, "cursor setup spawn returned null");
+ if (spawned != null)
+ blockedTargets[i] = spawned.Target;
+ }
+
+ BenchmarkObject cursorObject = pool.Spawn("cursor");
+ AssertNotNull(cursorObject, "cursor target spawn returned null");
+ if (cursorObject != null)
+ pool.Unspawn(cursorObject.Target);
+
+ RestartCaseMeasure();
+ pool.ReleaseAllUnused();
+ if (blockedTargets[0] != null)
+ pool.Unspawn(blockedTargets[0]);
+ BenchmarkObject recovered = pool.Spawn("cursor");
+ AssertNotNull(recovered, "cursor did not recover after release");
+ if (recovered != null)
+ pool.Unspawn(recovered.Target);
+ StopCaseMeasure();
+
+ for (int i = 1; i < blockedTargets.Length; i++)
+ pool.Unspawn(blockedTargets[i]);
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunLockedReleaseGuard()
+ {
+ string poolName = MakePoolName("locked");
+ IObjectPool pool = CreatePool(poolName, false, 4, float.MaxValue);
+ BenchmarkTarget target = new BenchmarkTarget(6);
+ BenchmarkObject obj = BenchmarkObject.Create("locked", target, true, true);
+ pool.Register(obj, false);
+
+ RestartCaseMeasure();
+ pool.ReleaseAllUnused();
+
+ AssertEqual(pool.Count, 1, "locked object was released");
+ StopCaseMeasure();
+ DestroyPool(poolName);
+ }
+
+ private void RunCustomReleaseGuard()
+ {
+ string poolName = MakePoolName("custom-release");
+ IObjectPool pool = CreatePool(poolName, false, 4, float.MaxValue);
+ BenchmarkTarget target = new BenchmarkTarget(7);
+ BenchmarkObject obj = BenchmarkObject.Create("custom", target, false, false);
+ pool.Register(obj, false);
+
+ RestartCaseMeasure();
+ pool.ReleaseAllUnused();
+
+ AssertEqual(pool.Count, 1, "custom release guard object was released");
+ StopCaseMeasure();
+ DestroyPool(poolName);
+ }
+
+ private void RunExpireRelease()
+ {
+ using (s_ExpireMarker.Auto())
+ {
+ string poolName = MakePoolName("expire");
+ IObjectPool pool = CreatePool(poolName, false, objectCount, 0f);
+ int count = Math.Max(1, Math.Min(objectCount, 4096));
+
+ for (int i = 0; i < count; i++)
+ pool.Register(BenchmarkObject.Create("expire", new BenchmarkTarget(i), false, true), false);
+
+ RestartCaseMeasure();
+ pool.ReleaseAllUnused();
+ AssertEqual(pool.Count, 0, "expire release did not clear all unused objects");
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private void RunReleaseAllUnused()
+ {
+ using (s_ReleaseMarker.Auto())
+ {
+ string poolName = MakePoolName("release-all");
+ IObjectPool pool = CreatePool(poolName, false, objectCount, float.MaxValue);
+
+ for (int i = 0; i < objectCount; i++)
+ pool.Register(BenchmarkObject.Create("release", new BenchmarkTarget(i), false, true), false);
+
+ RestartCaseMeasure();
+ pool.ReleaseAllUnused();
+ AssertEqual(pool.Count, 0, "ReleaseAllUnused did not clear pool");
+ StopCaseMeasure();
+
+ DestroyPool(poolName);
+ }
+ }
+
+ private IObjectPool CreatePool(string poolName, bool multiSpawn, int capacity, float expireTime)
+ {
+ if (m_Service.HasObjectPool(poolName))
+ m_Service.DestroyObjectPool(poolName);
+
+ return m_Service.CreatePool(
+ new ObjectPoolCreateOptions(poolName, multiSpawn, float.MaxValue, capacity, expireTime, 0));
+ }
+
+ private void DestroyPool(string poolName)
+ {
+ if (m_Service.HasObjectPool(poolName))
+ m_Service.DestroyObjectPool(poolName);
+ }
+
+ private static string MakePoolName(string suffix)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append("ObjectPoolBenchmark.");
+ builder.Append(suffix);
+ return builder.ToString();
+ }
+ }
+
+ private static string MakeIndexedName(string prefix, int index)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(prefix);
+ builder.Append('.');
+ builder.Append(index);
+ return builder.ToString();
+ }
+ }
+
+ private void AssertNotNull(object value, string message)
+ {
+ if (value != null)
+ return;
+
+ m_FailCount++;
+ Debug.LogError(message);
+ }
+
+ private void AssertReference(object actual, object expected, string message)
+ {
+ if (ReferenceEquals(actual, expected))
+ return;
+
+ m_FailCount++;
+ Debug.LogError(message);
+ }
+
+ private void AssertEqual(int actual, int expected, string message)
+ {
+ if (actual == expected)
+ return;
+
+ m_FailCount++;
+ Debug.LogError(BuildLog(message, " actual=", actual, ", expected=", expected));
+ }
+
+ private static string BuildLog(object a, string b, object c, string d,object e)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(a);
+ builder.Append(b);
+ builder.Append(c);
+ builder.Append(d);
+ builder.Append(e);
+ return builder.ToString();
+ }
+ }
+
+ private static string BuildLog(string a, object b, string c, object d)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(a);
+ builder.Append(b);
+ builder.Append(c);
+ builder.Append(d);
+ return builder.ToString();
+ }
+ }
+
+ private static string BuildLog(string a, object b, string c, object d, string e, object f)
+ {
+ using (var builder = ZString.CreateStringBuilder())
+ {
+ builder.Append(a);
+ builder.Append(b);
+ builder.Append(c);
+ builder.Append(d);
+ builder.Append(e);
+ builder.Append(f);
+ return builder.ToString();
+ }
+ }
+
+ private sealed class BenchmarkTarget
+ {
+ public readonly int Id;
+
+ public BenchmarkTarget(int id)
+ {
+ Id = id;
+ }
+ }
+
+ private sealed class BenchmarkObject : ObjectBase
+ {
+ private bool m_CustomCanReleaseFlag;
+
+ public override bool CustomCanReleaseFlag => m_CustomCanReleaseFlag;
+
+ public BenchmarkObject()
+ {
+ }
+
+ public static BenchmarkObject Create(string name, BenchmarkTarget target, bool locked, bool customCanReleaseFlag)
+ {
+ BenchmarkObject obj = MemoryPool.Acquire();
+ obj.Initialize(name, target, locked);
+ obj.m_CustomCanReleaseFlag = customCanReleaseFlag;
+ return obj;
+ }
+
+ protected internal override void Release(bool isShutdown)
+ {
+ }
+
+ public override void Clear()
+ {
+ base.Clear();
+ m_CustomCanReleaseFlag = true;
+ }
+ }
+ }
+}
+
+#endif
diff --git a/Runtime/ObjectPool/IObjectPoolDebugView.cs.meta b/Runtime/ObjectPool/Benchmark/ObjectPoolBenchmark.cs.meta
similarity index 83%
rename from Runtime/ObjectPool/IObjectPoolDebugView.cs.meta
rename to Runtime/ObjectPool/Benchmark/ObjectPoolBenchmark.cs.meta
index 87ba027..a3971b4 100644
--- a/Runtime/ObjectPool/IObjectPoolDebugView.cs.meta
+++ b/Runtime/ObjectPool/Benchmark/ObjectPoolBenchmark.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: b99f3fc658c4a3d4f80218ab7113341e
+guid: 472607d053c8822499f9960bb6ef21bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/ObjectPool/IObjectPoolDebugView.cs b/Runtime/ObjectPool/IObjectPoolDebugView.cs
deleted file mode 100644
index f310ec2..0000000
--- a/Runtime/ObjectPool/IObjectPoolDebugView.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace AlicizaX.ObjectPool
-{
- internal interface IObjectPoolDebugView
- {
- int GetAllObjectInfos(ObjectInfo[] results);
- }
-}
diff --git a/Runtime/ObjectPool/IObjectPoolServiceDebugView.cs b/Runtime/ObjectPool/IObjectPoolServiceDebugView.cs
deleted file mode 100644
index f2d0a56..0000000
--- a/Runtime/ObjectPool/IObjectPoolServiceDebugView.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace AlicizaX.ObjectPool
-{
- internal interface IObjectPoolServiceDebugView
- {
- int GetAllObjectPools(bool sort, ObjectPoolBase[] results);
- }
-}
diff --git a/Runtime/ObjectPool/ObjectBase.cs b/Runtime/ObjectPool/ObjectBase.cs
index 9ae0c1d..ff22228 100644
--- a/Runtime/ObjectPool/ObjectBase.cs
+++ b/Runtime/ObjectPool/ObjectBase.cs
@@ -54,4 +54,24 @@ namespace AlicizaX.ObjectPool
m_LastUseTime = 0f;
}
}
+
+ public abstract class ObjectBase : ObjectBase where TTarget : class
+ {
+ public new TTarget Target => (TTarget)base.Target;
+
+ protected void Initialize(TTarget target)
+ {
+ base.Initialize(string.Empty, target, false);
+ }
+
+ protected void Initialize(string name, TTarget target)
+ {
+ base.Initialize(name, target, false);
+ }
+
+ protected void Initialize(string name, TTarget target, bool locked)
+ {
+ base.Initialize(name, target, locked);
+ }
+ }
}
diff --git a/Runtime/ObjectPool/ObjectPoolBase.cs b/Runtime/ObjectPool/ObjectPoolBase.cs
index c5352bc..8a7ab5e 100644
--- a/Runtime/ObjectPool/ObjectPoolBase.cs
+++ b/Runtime/ObjectPool/ObjectPoolBase.cs
@@ -2,10 +2,11 @@ using System;
namespace AlicizaX.ObjectPool
{
- public abstract class ObjectPoolBase : IObjectPoolDebugView
+ public abstract class ObjectPoolBase
{
private readonly string m_Name;
private string m_FullName;
+ internal bool IsActive;
public ObjectPoolBase() : this(null) { }
diff --git a/Runtime/ObjectPool/ObjectPoolComponent.cs b/Runtime/ObjectPool/ObjectPoolComponent.cs
index dfd3ccc..cb91493 100644
--- a/Runtime/ObjectPool/ObjectPoolComponent.cs
+++ b/Runtime/ObjectPool/ObjectPoolComponent.cs
@@ -32,8 +32,8 @@ namespace AlicizaX
internal int GetAllObjectPools(bool sort, ObjectPoolBase[] results)
{
- if (_mObjectPoolService is IObjectPoolServiceDebugView debugView)
- return debugView.GetAllObjectPools(sort, results);
+ if (_mObjectPoolService is ObjectPoolService svc)
+ return svc.GetAllObjectPools(sort, results);
return 0;
}
diff --git a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs
index f7b0231..fcfb6c6 100644
--- a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs
+++ b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs
@@ -17,8 +17,6 @@ namespace AlicizaX.ObjectPool
public float LastUseTime;
public int PrevByName;
public int NextByName;
- public int PrevAvailableByName;
- public int NextAvailableByName;
public int PrevUnused;
public int NextUnused;
public byte Flags;
@@ -41,8 +39,7 @@ namespace AlicizaX.ObjectPool
private ReferenceOpenHashMap m_TargetMap;
private StringOpenHashMap m_AllNameHeadMap;
- private StringOpenHashMap m_AvailableNameHeadMap;
- private StringOpenHashMap m_AvailableNameTailMap;
+ private StringOpenHashMap m_NameCursorMap;
private readonly bool m_AllowMultiSpawn;
private float m_AutoReleaseInterval;
@@ -72,8 +69,7 @@ namespace AlicizaX.ObjectPool
m_FreeStack = SlotArrayPool.Rent(initCap);
m_TargetMap = new ReferenceOpenHashMap(initCap);
m_AllNameHeadMap = new StringOpenHashMap(initCap);
- m_AvailableNameHeadMap = new StringOpenHashMap(initCap);
- m_AvailableNameTailMap = new StringOpenHashMap(initCap);
+ m_NameCursorMap = new StringOpenHashMap(initCap);
m_AllowMultiSpawn = allowMultiSpawn;
m_AutoReleaseInterval = autoReleaseInterval;
m_Capacity = capacity;
@@ -167,15 +163,24 @@ namespace AlicizaX.ObjectPool
return;
}
+ if (!EnsureRegisterCapacity())
+ {
+#if UNITY_EDITOR
+ UnityEngine.Debug.LogError($"Object pool '{FullName}' capacity is full.");
+#endif
+ return;
+ }
+
int idx = AllocSlot();
+ if (idx < 0)
+ return;
+
ref var slot = ref m_Slots[idx];
slot.Obj = obj;
slot.SpawnCount = spawned ? 1 : 0;
slot.LastUseTime = Time.realtimeSinceStartup;
slot.PrevByName = -1;
slot.NextByName = -1;
- slot.PrevAvailableByName = -1;
- slot.NextAvailableByName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
slot.SetAlive(true);
@@ -188,6 +193,10 @@ namespace AlicizaX.ObjectPool
m_Slots[existingHead].PrevByName = idx;
slot.NextByName = existingHead;
}
+ else
+ {
+ m_NameCursorMap.AddOrUpdate(objectName, idx);
+ }
m_AllNameHeadMap.AddOrUpdate(objectName, idx);
obj.LastUseTime = slot.LastUseTime;
@@ -196,7 +205,7 @@ namespace AlicizaX.ObjectPool
else
MarkSlotAvailable(idx);
- if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
+ UpdateActiveState();
ValidateState();
}
@@ -205,16 +214,18 @@ namespace AlicizaX.ObjectPool
public T Spawn(string name)
{
- if (name == null) return null;
+ if (name == null) name = string.Empty;
if (m_AllowMultiSpawn)
return SpawnAny(name);
- if (!m_AvailableNameHeadMap.TryGetValue(name, out int head)) return null;
+ int head = FindAvailableByName(name);
+ if (head < 0) return null;
+
ref var slot = ref m_Slots[head];
if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
{
#if UNITY_EDITOR
- UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is inconsistent.");
+ UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain is inconsistent.");
#endif
return null;
}
@@ -230,11 +241,64 @@ namespace AlicizaX.ObjectPool
public bool CanSpawn(string name)
{
- if (name == null) return false;
+ if (name == null) name = string.Empty;
if (m_AllowMultiSpawn)
return m_AllNameHeadMap.ContainsKey(name);
- return m_AvailableNameHeadMap.ContainsKey(name);
+ return FindAvailableByName(name) >= 0;
+ }
+
+ private int FindAvailableByName(string name)
+ {
+ if (!m_AllNameHeadMap.TryGetValue(name, out int head))
+ return -1;
+
+ ref var headSlot = ref m_Slots[head];
+ if (headSlot.IsAlive()
+ && headSlot.SpawnCount == 0
+ && string.Equals(headSlot.Obj.Name, name, StringComparison.Ordinal))
+ {
+ return head;
+ }
+
+ int current = GetValidNameCursor(name, head);
+ if (current == head)
+ current = headSlot.NextByName >= 0 ? headSlot.NextByName : head;
+
+ int first = current;
+
+ do
+ {
+ ref var slot = ref m_Slots[current];
+ if (slot.IsAlive()
+ && slot.SpawnCount == 0
+ && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
+ {
+ int nextCursor = slot.NextByName >= 0 ? slot.NextByName : head;
+ m_NameCursorMap.AddOrUpdate(name, nextCursor);
+ return current;
+ }
+
+ current = slot.NextByName >= 0 ? slot.NextByName : head;
+ }
+ while (current != first);
+
+ return -1;
+ }
+
+ private int GetValidNameCursor(string name, int head)
+ {
+ if (m_NameCursorMap.TryGetValue(name, out int cursor)
+ && cursor >= 0
+ && cursor < m_SlotCount)
+ {
+ ref var slot = ref m_Slots[cursor];
+ if (slot.IsAlive() && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
+ return cursor;
+ }
+
+ m_NameCursorMap.AddOrUpdate(name, head);
+ return head;
}
public void Unspawn(T obj)
@@ -279,7 +343,7 @@ namespace AlicizaX.ObjectPool
ref var slot = ref m_Slots[current];
if (CanReleaseSlot(ref slot))
{
- ReleaseSlot(current);
+ ReleaseSlot(current, false);
released++;
}
current = next;
@@ -288,7 +352,9 @@ namespace AlicizaX.ObjectPool
if (released > 0)
{
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
+ TrimSlotCountTail();
ShrinkStorageIfEmpty();
+ UpdateActiveState();
ValidateState();
}
}
@@ -306,6 +372,7 @@ namespace AlicizaX.ObjectPool
if (m_PendingReleaseCount <= 0 && !checkExpire)
{
TryProgressiveShrink();
+ UpdateActiveState();
return;
}
@@ -323,6 +390,7 @@ namespace AlicizaX.ObjectPool
}
TryProgressiveShrink();
+ UpdateActiveState();
}
private void TryProgressiveShrink()
@@ -333,14 +401,17 @@ namespace AlicizaX.ObjectPool
m_ShrinkCounter = 0;
+ TrimSlotCountTail();
+
int slotArrayLen = m_Slots.Length;
- if (m_TargetMap.Count == 0 || slotArrayLen <= InitSlotCapacity)
+ int aliveCount = m_TargetMap.Count;
+ if (aliveCount == 0 || slotArrayLen <= InitSlotCapacity)
return;
- float usageRatio = (float)m_TargetMap.Count / slotArrayLen;
+ float usageRatio = (float)aliveCount / slotArrayLen;
if (usageRatio < 0.25f)
{
- int targetCapacity = Math.Max(slotArrayLen / 2, InitSlotCapacity);
+ int targetCapacity = Math.Max(NextPowerOf2(Math.Max(m_SlotCount, aliveCount)), InitSlotCapacity);
if (targetCapacity < slotArrayLen && m_SlotCount <= targetCapacity)
{
var newSlots = SlotArrayPool.Rent(targetCapacity);
@@ -380,8 +451,7 @@ namespace AlicizaX.ObjectPool
m_TargetMap.Clear();
m_AllNameHeadMap.Clear();
- m_AvailableNameHeadMap.Clear();
- m_AvailableNameTailMap.Clear();
+ m_NameCursorMap.Clear();
SlotArrayPool.Return(m_Slots, true);
SlotArrayPool.Return(m_FreeStack, true);
@@ -482,14 +552,23 @@ namespace AlicizaX.ObjectPool
return m_FreeStack[--m_FreeTop];
if (m_SlotCount >= m_Slots.Length)
+ {
GrowSlots();
+ if (m_SlotCount >= m_Slots.Length)
+ return -1;
+ }
return m_SlotCount++;
}
private void GrowSlots()
{
- int newCap = Math.Max(m_Slots.Length * 2, InitSlotCapacity);
+ int currentCap = m_Slots.Length;
+ int maxCap = m_Capacity == int.MaxValue ? int.MaxValue : Math.Max(m_Capacity, InitSlotCapacity);
+ int newCap = Math.Min(Math.Max(currentCap * 2, InitSlotCapacity), maxCap);
+ if (newCap <= currentCap)
+ return;
+
var newSlots = SlotArrayPool.Rent(newCap);
var newFreeStack = SlotArrayPool.Rent(newCap);
@@ -503,14 +582,14 @@ namespace AlicizaX.ObjectPool
m_FreeStack = newFreeStack;
}
- private void ReleaseSlot(int idx)
+ private void ReleaseSlot(int idx, bool compactStorage = true)
{
ref var slot = ref m_Slots[idx];
if (!slot.IsAlive()) return;
+ if (slot.SpawnCount > 0) return;
T obj = slot.Obj;
- if (slot.SpawnCount == 0)
- MarkSlotUnavailable(idx);
+ MarkSlotUnavailable(idx);
RemoveFromAllNameChain(idx);
m_TargetMap.Remove(obj.Target);
@@ -523,8 +602,6 @@ namespace AlicizaX.ObjectPool
slot.SpawnCount = 0;
slot.PrevByName = -1;
slot.NextByName = -1;
- slot.PrevAvailableByName = -1;
- slot.NextAvailableByName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
@@ -538,7 +615,53 @@ namespace AlicizaX.ObjectPool
}
m_FreeStack[m_FreeTop++] = idx;
- ShrinkStorageIfEmpty();
+ if (compactStorage)
+ {
+ TrimSlotCountTail();
+ ShrinkStorageIfEmpty();
+ }
+ }
+
+ private bool EnsureRegisterCapacity()
+ {
+ if (m_Capacity == int.MaxValue || Count < m_Capacity)
+ return true;
+
+ int released = ReleaseUnused(1, false, float.MinValue);
+ if (released > 0)
+ {
+ m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
+ return Count < m_Capacity;
+ }
+
+ return false;
+ }
+
+ private void TrimSlotCountTail()
+ {
+ while (m_SlotCount > 0 && !m_Slots[m_SlotCount - 1].IsAlive())
+ m_SlotCount--;
+
+ int write = 0;
+ for (int i = 0; i < m_FreeTop; i++)
+ {
+ int freeIndex = m_FreeStack[i];
+ if (freeIndex < m_SlotCount)
+ m_FreeStack[write++] = freeIndex;
+ }
+ m_FreeTop = write;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int NextPowerOf2(int value)
+ {
+ value--;
+ value |= value >> 1;
+ value |= value >> 2;
+ value |= value >> 4;
+ value |= value >> 8;
+ value |= value >> 16;
+ return value + 1;
}
private void RemoveFromAllNameChain(int idx)
@@ -570,6 +693,13 @@ namespace AlicizaX.ObjectPool
m_AllNameHeadMap.Remove(objectName);
}
+ if (m_NameCursorMap.TryGetValue(objectName, out int cursor) && cursor == idx)
+ {
+ if (next >= 0) m_NameCursorMap.AddOrUpdate(objectName, next);
+ else if (prev >= 0) m_NameCursorMap.AddOrUpdate(objectName, prev);
+ else m_NameCursorMap.Remove(objectName);
+ }
+
if (next >= 0)
m_Slots[next].PrevByName = prev;
@@ -589,12 +719,13 @@ namespace AlicizaX.ObjectPool
if (requireExpired && slot.LastUseTime > expireThreshold)
{
- break;
+ current = next;
+ continue;
}
if (CanReleaseSlot(ref slot))
{
- ReleaseSlot(current);
+ ReleaseSlot(current, false);
released++;
}
@@ -606,6 +737,12 @@ namespace AlicizaX.ObjectPool
m_LastBudgetScanStart = current >= 0 ? current : m_UnusedHead;
}
+ if (released > 0)
+ {
+ TrimSlotCountTail();
+ ShrinkStorageIfEmpty();
+ }
+
return released;
}
@@ -649,8 +786,7 @@ namespace AlicizaX.ObjectPool
m_Slots = SlotArrayPool.Rent(InitSlotCapacity);
m_FreeStack = SlotArrayPool.Rent(InitSlotCapacity);
m_AllNameHeadMap.Clear();
- m_AvailableNameHeadMap.Clear();
- m_AvailableNameTailMap.Clear();
+ m_NameCursorMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_UnusedHead = -1;
@@ -658,12 +794,10 @@ namespace AlicizaX.ObjectPool
m_LastBudgetScanStart = -1;
}
- [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ [Conditional("UNITY_EDITOR")]
private void ValidateState()
{
-#if !ENABLE_OBJECTPOOL_VALIDATION
- return;
-#else
+#if UNITY_EDITOR && ENABLE_OBJECTPOOL_VALIDATION
int aliveCount = 0;
int unusedCount = 0;
for (int i = 0; i < m_SlotCount; i++)
@@ -699,7 +833,6 @@ namespace AlicizaX.ObjectPool
}
bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0;
- bool inAvailableList = false;
if (slot.SpawnCount == 0)
{
@@ -708,24 +841,6 @@ namespace AlicizaX.ObjectPool
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent.");
}
-
- if (!m_AvailableNameHeadMap.TryGetValue(objectName, out int availableHead))
- {
- UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is missing.");
- }
- else
- {
- inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0;
- if (!inAvailableList)
- {
- UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent.");
- }
-
- if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i)
- {
- UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name link is inconsistent.");
- }
- }
}
else
{
@@ -733,11 +848,6 @@ namespace AlicizaX.ObjectPool
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list.");
}
-
- if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0)
- {
- UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in available chain.");
- }
}
}
@@ -776,13 +886,14 @@ namespace AlicizaX.ObjectPool
private void MarkSlotAvailable(int idx)
{
AddToUnusedListTail(idx);
- AddToAvailableNameChain(idx);
+ ref var slot = ref m_Slots[idx];
+ if (slot.IsAlive())
+ m_NameCursorMap.AddOrUpdate(slot.Obj.Name ?? string.Empty, idx);
}
private void MarkSlotUnavailable(int idx)
{
RemoveFromUnusedList(idx);
- RemoveFromAvailableNameChain(idx);
}
private void AddToUnusedListTail(int idx)
@@ -828,66 +939,6 @@ namespace AlicizaX.ObjectPool
m_LastBudgetScanStart = next >= 0 ? next : m_UnusedHead;
}
- private void AddToAvailableNameChain(int idx)
- {
- ref var slot = ref m_Slots[idx];
- if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0)
- {
-#if UNITY_EDITOR
- UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent.");
-#endif
- return;
- }
-
- string objectName = slot.Obj.Name ?? string.Empty;
- if (m_AvailableNameTailMap.TryGetValue(objectName, out int tail))
- {
- m_Slots[tail].NextAvailableByName = idx;
- slot.PrevAvailableByName = tail;
- slot.NextAvailableByName = -1;
- m_AvailableNameTailMap.AddOrUpdate(objectName, idx);
- }
- else
- {
- slot.PrevAvailableByName = -1;
- slot.NextAvailableByName = -1;
- m_AvailableNameHeadMap.AddOrUpdate(objectName, idx);
- m_AvailableNameTailMap.AddOrUpdate(objectName, idx);
- }
- }
-
- private void RemoveFromAvailableNameChain(int idx)
- {
- ref var slot = ref m_Slots[idx];
- string objectName = slot.Obj.Name ?? string.Empty;
- if (slot.PrevAvailableByName < 0
- && slot.NextAvailableByName < 0
- && (!m_AvailableNameHeadMap.TryGetValue(objectName, out int head) || head != idx))
- {
- return;
- }
-
- int prev = slot.PrevAvailableByName;
- int next = slot.NextAvailableByName;
-
- if (prev >= 0)
- m_Slots[prev].NextAvailableByName = next;
- else if (next >= 0)
- m_AvailableNameHeadMap.AddOrUpdate(objectName, next);
- else
- m_AvailableNameHeadMap.Remove(objectName);
-
- if (next >= 0)
- m_Slots[next].PrevAvailableByName = prev;
- else if (prev >= 0)
- m_AvailableNameTailMap.AddOrUpdate(objectName, prev);
- else
- m_AvailableNameTailMap.Remove(objectName);
-
- slot.PrevAvailableByName = -1;
- slot.NextAvailableByName = -1;
- }
-
private T SpawnAny(string name)
{
if (!m_AllNameHeadMap.TryGetValue(name, out int head))
@@ -902,6 +953,12 @@ namespace AlicizaX.ObjectPool
ValidateState();
return slot.Obj;
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void UpdateActiveState()
+ {
+ IsActive = m_TargetMap.Count > 0 || m_PendingReleaseCount > 0;
+ }
}
}
}
diff --git a/Runtime/ObjectPool/ObjectPoolService.cs b/Runtime/ObjectPool/ObjectPoolService.cs
index 967f754..c0439c1 100644
--- a/Runtime/ObjectPool/ObjectPoolService.cs
+++ b/Runtime/ObjectPool/ObjectPoolService.cs
@@ -1,60 +1,68 @@
using System;
-using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX.ObjectPool
{
[UnityEngine.Scripting.Preserve]
- internal sealed partial class ObjectPoolService : ServiceBase, IObjectPoolService, IObjectPoolServiceDebugView, IServiceTickable
+ internal sealed partial class ObjectPoolService : ServiceBase, IObjectPoolService, IServiceTickable
{
private const float DefaultAutoReleaseInterval = float.MaxValue;
private const int DefaultCapacity = int.MaxValue;
private const float DefaultExpireTime = float.MaxValue;
+ private const int InitPoolArrayCapacity = 8;
- private readonly Dictionary m_ObjectPools;
- private readonly List m_ObjectPoolList;
- private readonly Dictionary m_ObjectPoolIndexMap;
- private readonly List m_CachedSortedObjectPools;
- private readonly Comparison m_ObjectPoolComparer;
+ private TypeNamePairOpenHashMap m_PoolMap;
+ private ReferenceOpenHashMap m_PoolRefMap;
+ private ObjectPoolBase[] m_Pools;
+ private int m_PoolCount;
+ private ObjectPoolBase[] m_CachedSortedPools;
+ private int m_CachedSortedCount;
public ObjectPoolService()
{
- m_ObjectPools = new Dictionary();
- m_ObjectPoolList = new List();
- m_ObjectPoolIndexMap = new Dictionary(AlicizaX.ReferenceComparer.Instance);
- m_CachedSortedObjectPools = new List();
- m_ObjectPoolComparer = ObjectPoolComparer;
+ m_PoolMap = new TypeNamePairOpenHashMap(InitPoolArrayCapacity);
+ m_PoolRefMap = new ReferenceOpenHashMap(InitPoolArrayCapacity);
+ m_Pools = new ObjectPoolBase[InitPoolArrayCapacity];
+ m_PoolCount = 0;
+ m_CachedSortedPools = Array.Empty();
+ m_CachedSortedCount = 0;
}
public int Priority => 1;
- public int Count => m_ObjectPools.Count;
+ public int Count => m_PoolMap.Count;
void IServiceTickable.Tick(float deltaTime)
{
- for (int i = 0; i < m_ObjectPoolList.Count; i++)
- m_ObjectPoolList[i].Update(deltaTime, Time.unscaledDeltaTime);
+ float unscaled = Time.unscaledDeltaTime;
+ for (int i = 0; i < m_PoolCount; i++)
+ {
+ var pool = m_Pools[i];
+ if (pool.IsActive)
+ pool.Update(deltaTime, unscaled);
+ }
}
protected override void OnInitialize() { }
protected override void OnDestroyService()
{
- for (int i = m_ObjectPoolList.Count - 1; i >= 0; i--)
- m_ObjectPoolList[i].Shutdown();
- m_ObjectPools.Clear();
- m_ObjectPoolList.Clear();
- m_ObjectPoolIndexMap.Clear();
- m_CachedSortedObjectPools.Clear();
+ for (int i = m_PoolCount - 1; i >= 0; i--)
+ m_Pools[i].Shutdown();
+ m_PoolMap.Clear();
+ m_PoolRefMap.Clear();
+ Array.Clear(m_Pools, 0, m_PoolCount);
+ m_PoolCount = 0;
+ m_CachedSortedCount = 0;
}
// ========== Has ==========
public bool HasObjectPool() where T : ObjectBase
- => m_ObjectPools.ContainsKey(new TypeNamePair(typeof(T)));
+ => m_PoolMap.ContainsKey(new TypeNamePair(typeof(T)));
public bool HasObjectPool(string name) where T : ObjectBase
- => m_ObjectPools.ContainsKey(new TypeNamePair(typeof(T), name));
+ => m_PoolMap.ContainsKey(new TypeNamePair(typeof(T), name));
// ========== Get ==========
@@ -66,7 +74,7 @@ namespace AlicizaX.ObjectPool
// ========== GetAll ==========
- int IObjectPoolServiceDebugView.GetAllObjectPools(bool sort, ObjectPoolBase[] results)
+ internal int GetAllObjectPools(bool sort, ObjectPoolBase[] results)
{
if (results == null)
{
@@ -76,18 +84,17 @@ namespace AlicizaX.ObjectPool
return 0;
}
- List source = m_ObjectPoolList;
if (sort)
{
CacheSortedObjectPools();
- source = m_CachedSortedObjectPools;
+ int copyCount = results.Length < m_CachedSortedCount ? results.Length : m_CachedSortedCount;
+ Array.Copy(m_CachedSortedPools, 0, results, 0, copyCount);
+ return m_CachedSortedCount;
}
- int count = source.Count;
- int copyCount = results.Length < count ? results.Length : count;
- for (int i = 0; i < copyCount; i++)
- results[i] = source[i];
-
+ int count = m_PoolCount;
+ int copy = results.Length < count ? results.Length : count;
+ Array.Copy(m_Pools, 0, results, 0, copy);
return count;
}
@@ -96,7 +103,7 @@ namespace AlicizaX.ObjectPool
public IObjectPool CreatePool(ObjectPoolCreateOptions options = default) where T : ObjectBase
{
var key = new TypeNamePair(typeof(T), options.Name);
- if (m_ObjectPools.ContainsKey(key))
+ if (m_PoolMap.ContainsKey(key))
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Already exist object pool '{key}'.");
@@ -112,9 +119,17 @@ namespace AlicizaX.ObjectPool
options.ExpireTime ?? DefaultExpireTime,
options.Priority);
- m_ObjectPools.Add(key, pool);
- m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count);
- m_ObjectPoolList.Add(pool);
+ int idx = m_PoolCount;
+ if (idx >= m_Pools.Length)
+ {
+ var newArr = new ObjectPoolBase[m_Pools.Length * 2];
+ Array.Copy(m_Pools, 0, newArr, 0, m_PoolCount);
+ m_Pools = newArr;
+ }
+ m_Pools[idx] = pool;
+ m_PoolCount++;
+ m_PoolMap.AddOrUpdate(key, idx);
+ m_PoolRefMap.AddOrUpdate(pool, idx);
return pool;
}
@@ -143,67 +158,81 @@ namespace AlicizaX.ObjectPool
public void Release()
{
CacheSortedObjectPools();
- for (int i = 0; i < m_CachedSortedObjectPools.Count; i++)
- m_CachedSortedObjectPools[i].Release();
+ for (int i = 0; i < m_CachedSortedCount; i++)
+ m_CachedSortedPools[i].Release();
}
public void ReleaseAllUnused()
{
CacheSortedObjectPools();
- for (int i = 0; i < m_CachedSortedObjectPools.Count; i++)
- m_CachedSortedObjectPools[i].ReleaseAllUnused();
+ for (int i = 0; i < m_CachedSortedCount; i++)
+ m_CachedSortedPools[i].ReleaseAllUnused();
}
// ========== Low memory ==========
public void OnLowMemory()
{
- for (int i = 0; i < m_ObjectPoolList.Count; i++)
- m_ObjectPoolList[i].OnLowMemory();
+ for (int i = 0; i < m_PoolCount; i++)
+ m_Pools[i].OnLowMemory();
}
// ========== Internal ==========
private ObjectPoolBase InternalGet(TypeNamePair key)
{
- m_ObjectPools.TryGetValue(key, out var pool);
- return pool;
+ if (m_PoolMap.TryGetValue(key, out int idx))
+ return m_Pools[idx];
+ return null;
}
private bool InternalDestroy(TypeNamePair key)
{
- if (m_ObjectPools.TryGetValue(key, out var pool))
+ if (!m_PoolMap.TryGetValue(key, out int idx))
+ return false;
+
+ var pool = m_Pools[idx];
+ pool.Shutdown();
+
+ int lastIndex = m_PoolCount - 1;
+ if (idx < lastIndex)
{
- pool.Shutdown();
- RemovePoolFromList(pool);
- m_ObjectPoolIndexMap.Remove(pool);
- return m_ObjectPools.Remove(key);
+ var lastPool = m_Pools[lastIndex];
+ m_Pools[idx] = lastPool;
+ m_PoolRefMap.AddOrUpdate(lastPool, idx);
+ var lastKey = new TypeNamePair(lastPool.ObjectType, lastPool.Name);
+ m_PoolMap.AddOrUpdate(lastKey, idx);
}
- return false;
+ m_Pools[lastIndex] = null;
+ m_PoolCount--;
+
+ m_PoolMap.Remove(key);
+ m_PoolRefMap.Remove(pool);
+ return true;
}
private void CacheSortedObjectPools()
{
- m_CachedSortedObjectPools.Clear();
- m_CachedSortedObjectPools.AddRange(m_ObjectPoolList);
- m_CachedSortedObjectPools.Sort(m_ObjectPoolComparer);
+ int count = m_PoolCount;
+ if (m_CachedSortedPools.Length < count)
+ m_CachedSortedPools = new ObjectPoolBase[Math.Max(count, 8)];
+
+ Array.Copy(m_Pools, 0, m_CachedSortedPools, 0, count);
+ m_CachedSortedCount = count;
+
+ for (int i = 1; i < count; i++)
+ {
+ var key = m_CachedSortedPools[i];
+ int keyPriority = key.Priority;
+ int j = i - 1;
+ while (j >= 0 && m_CachedSortedPools[j].Priority > keyPriority)
+ {
+ m_CachedSortedPools[j + 1] = m_CachedSortedPools[j];
+ j--;
+ }
+ m_CachedSortedPools[j + 1] = key;
+ }
}
- private void RemovePoolFromList(ObjectPoolBase pool)
- {
- if (!m_ObjectPoolIndexMap.TryGetValue(pool, out int index))
- return;
-
- int lastIndex = m_ObjectPoolList.Count - 1;
- ObjectPoolBase lastPool = m_ObjectPoolList[lastIndex];
- m_ObjectPoolList[index] = lastPool;
- m_ObjectPoolList.RemoveAt(lastIndex);
-
- if (!ReferenceEquals(lastPool, pool))
- m_ObjectPoolIndexMap[lastPool] = index;
- }
-
- private static int ObjectPoolComparer(ObjectPoolBase a, ObjectPoolBase b)
- => a.Priority.CompareTo(b.Priority);
}
}
diff --git a/Runtime/ObjectPool/SlotArrayPool.cs b/Runtime/ObjectPool/SlotArrayPool.cs
index 41a41ca..bbd4bef 100644
--- a/Runtime/ObjectPool/SlotArrayPool.cs
+++ b/Runtime/ObjectPool/SlotArrayPool.cs
@@ -8,17 +8,15 @@ namespace AlicizaX.ObjectPool
///
internal static class SlotArrayPool
{
- private static readonly ArrayPool s_Pool = ArrayPool.Create(256, 50);
-
public static T[] Rent(int minimumLength)
{
- return s_Pool.Rent(minimumLength);
+ return ArrayPool.Shared.Rent(minimumLength);
}
public static void Return(T[] array, bool clearArray = false)
{
if (array != null)
- s_Pool.Return(array, clearArray);
+ ArrayPool.Shared.Return(array, clearArray);
}
}
}
diff --git a/Runtime/ObjectPool/StringOpenHashMap.cs b/Runtime/ObjectPool/StringOpenHashMap.cs
index f5755da..df432dc 100644
--- a/Runtime/ObjectPool/StringOpenHashMap.cs
+++ b/Runtime/ObjectPool/StringOpenHashMap.cs
@@ -47,7 +47,7 @@ namespace AlicizaX.ObjectPool
while (i > 0)
{
int idx = i - 1;
- if (m_Keys[idx] == key) { value = m_Values[idx]; return true; }
+ if (string.Equals(m_Keys[idx], key, StringComparison.Ordinal)) { value = m_Values[idx]; return true; }
i = m_Next[idx];
}
value = -1;
@@ -67,7 +67,7 @@ namespace AlicizaX.ObjectPool
while (i > 0)
{
int ei = i - 1;
- if (m_Keys[ei] == key) { m_Values[ei] = value; return; }
+ if (string.Equals(m_Keys[ei], key, StringComparison.Ordinal)) { m_Values[ei] = value; return; }
i = m_Next[ei];
}
@@ -101,7 +101,7 @@ namespace AlicizaX.ObjectPool
while (i > 0)
{
int idx = i - 1;
- if (m_Keys[idx] == key)
+ if (string.Equals(m_Keys[idx], key, StringComparison.Ordinal))
{
if (prev == 0) m_Buckets[bucket] = m_Next[idx];
else m_Next[prev - 1] = m_Next[idx];
diff --git a/Runtime/ObjectPool/TypeNamePairOpenHashMap.cs b/Runtime/ObjectPool/TypeNamePairOpenHashMap.cs
new file mode 100644
index 0000000..f98d0e2
--- /dev/null
+++ b/Runtime/ObjectPool/TypeNamePairOpenHashMap.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace AlicizaX.ObjectPool
+{
+ internal struct TypeNamePairOpenHashMap
+ {
+ private int[] m_Buckets;
+ private TypeNamePair[] m_Keys;
+ private int[] m_Values;
+ private int[] m_Next;
+ private int m_Count;
+ private int m_FreeList;
+ private int m_Mask;
+ private int m_AllocCount;
+
+ private const int MinCapacity = 8;
+
+ public int Count => m_Count;
+
+ public TypeNamePairOpenHashMap(int capacity)
+ {
+ int cap = NextPowerOf2(Math.Max(capacity, MinCapacity));
+ m_Mask = cap - 1;
+ m_Buckets = SlotArrayPool.Rent(cap);
+ m_Keys = SlotArrayPool.Rent(cap);
+ m_Values = SlotArrayPool.Rent(cap);
+ m_Next = SlotArrayPool.Rent(cap);
+ Array.Clear(m_Buckets, 0, m_Buckets.Length);
+ Array.Clear(m_Keys, 0, m_Keys.Length);
+ Array.Clear(m_Values, 0, m_Values.Length);
+ Array.Clear(m_Next, 0, m_Next.Length);
+ m_Count = 0;
+ m_FreeList = 0;
+ m_AllocCount = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetValue(TypeNamePair key, out int value)
+ {
+ if (m_Buckets == null) { value = -1; return false; }
+ int hash = key.GetHashCode() & 0x7FFFFFFF;
+ int i = m_Buckets[hash & m_Mask];
+ while (i > 0)
+ {
+ int idx = i - 1;
+ if (m_Keys[idx].Equals(key)) { value = m_Values[idx]; return true; }
+ i = m_Next[idx];
+ }
+ value = -1;
+ return false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool ContainsKey(TypeNamePair key) => TryGetValue(key, out _);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddOrUpdate(TypeNamePair key, int value)
+ {
+ if (m_Count >= ((m_Mask + 1) * 3 >> 2))
+ Grow();
+
+ int hash = key.GetHashCode() & 0x7FFFFFFF;
+ int bucket = hash & m_Mask;
+ int i = m_Buckets[bucket];
+ while (i > 0)
+ {
+ int ei = i - 1;
+ if (m_Keys[ei].Equals(key)) { m_Values[ei] = value; return; }
+ i = m_Next[ei];
+ }
+
+ int idx;
+ if (m_FreeList > 0)
+ {
+ idx = m_FreeList - 1;
+ m_FreeList = m_Next[idx];
+ }
+ else
+ {
+ if (m_AllocCount > m_Mask) { Grow(); bucket = hash & m_Mask; }
+ idx = m_AllocCount++;
+ }
+
+ m_Keys[idx] = key;
+ m_Values[idx] = value;
+ m_Next[idx] = m_Buckets[bucket];
+ m_Buckets[bucket] = idx + 1;
+ m_Count++;
+ }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Remove(TypeNamePair key)
+ {
+ if (m_Buckets == null) return false;
+ int hash = key.GetHashCode() & 0x7FFFFFFF;
+ int bucket = hash & m_Mask;
+ int prev = 0;
+ int i = m_Buckets[bucket];
+ while (i > 0)
+ {
+ int idx = i - 1;
+ if (m_Keys[idx].Equals(key))
+ {
+ if (prev == 0) m_Buckets[bucket] = m_Next[idx];
+ else m_Next[prev - 1] = m_Next[idx];
+ m_Keys[idx] = default;
+ m_Values[idx] = -1;
+ m_Next[idx] = m_FreeList;
+ m_FreeList = idx + 1;
+ m_Count--;
+ return true;
+ }
+ prev = i;
+ i = m_Next[idx];
+ }
+ return false;
+ }
+
+ public void Clear()
+ {
+ if (m_Buckets == null) return;
+ int cap = m_Mask + 1;
+ Array.Clear(m_Buckets, 0, cap);
+ Array.Clear(m_Keys, 0, cap);
+ Array.Clear(m_Values, 0, cap);
+ Array.Clear(m_Next, 0, cap);
+ m_Count = 0;
+ m_FreeList = 0;
+ m_AllocCount = 0;
+ }
+
+ private void Grow()
+ {
+ int newCap = (m_Mask + 1) << 1;
+ if (newCap < MinCapacity) newCap = MinCapacity;
+ int newMask = newCap - 1;
+ var newBuckets = SlotArrayPool.Rent(newCap);
+ var newKeys = SlotArrayPool.Rent(newCap);
+ var newValues = SlotArrayPool.Rent(newCap);
+ var newNext = SlotArrayPool.Rent(newCap);
+ Array.Clear(newBuckets, 0, newBuckets.Length);
+ Array.Clear(newNext, 0, newNext.Length);
+
+ int newAlloc = 0;
+ int oldCap = m_Mask + 1;
+ for (int b = 0; b < oldCap; b++)
+ {
+ int i = m_Buckets[b];
+ while (i > 0)
+ {
+ int old = i - 1;
+ int ni = newAlloc++;
+ newKeys[ni] = m_Keys[old];
+ newValues[ni] = m_Values[old];
+ int hash = newKeys[ni].GetHashCode() & 0x7FFFFFFF;
+ int nb = hash & newMask;
+ newNext[ni] = newBuckets[nb];
+ newBuckets[nb] = ni + 1;
+ i = m_Next[old];
+ }
+ }
+
+ SlotArrayPool.Return(m_Buckets, true);
+ SlotArrayPool.Return(m_Keys, true);
+ SlotArrayPool.Return(m_Values, true);
+ SlotArrayPool.Return(m_Next, true);
+
+ m_Buckets = newBuckets;
+ m_Keys = newKeys;
+ m_Values = newValues;
+ m_Next = newNext;
+ m_Mask = newMask;
+ m_AllocCount = newAlloc;
+ m_FreeList = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int NextPowerOf2(int v)
+ {
+ v--;
+ v |= v >> 1; v |= v >> 2; v |= v >> 4;
+ v |= v >> 8; v |= v >> 16;
+ return v + 1;
+ }
+ }
+}
diff --git a/Runtime/ObjectPool/IObjectPoolServiceDebugView.cs.meta b/Runtime/ObjectPool/TypeNamePairOpenHashMap.cs.meta
similarity index 83%
rename from Runtime/ObjectPool/IObjectPoolServiceDebugView.cs.meta
rename to Runtime/ObjectPool/TypeNamePairOpenHashMap.cs.meta
index dadcadb..d626932 100644
--- a/Runtime/ObjectPool/IObjectPoolServiceDebugView.cs.meta
+++ b/Runtime/ObjectPool/TypeNamePairOpenHashMap.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 8e3fa6e0005d58a4eba0e005ee613c61
+guid: 42dbc1a039373c04abf61e06846ce201
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Resource/Resource/Extension/AssetItemObject.cs b/Runtime/Resource/Resource/Extension/AssetItemObject.cs
index 38557dd..2a8e2e0 100644
--- a/Runtime/Resource/Resource/Extension/AssetItemObject.cs
+++ b/Runtime/Resource/Resource/Extension/AssetItemObject.cs
@@ -1,10 +1,9 @@
-using System.Buffers;
using AlicizaX.ObjectPool;
using AlicizaX;
namespace AlicizaX.Resource.Runtime
{
- public class AssetItemObject : ObjectBase
+ public class AssetItemObject : ObjectBase
{
public static AssetItemObject Create(string location, UnityEngine.Object target)
{
@@ -15,7 +14,6 @@ namespace AlicizaX.Resource.Runtime
protected internal override void Release(bool isShutdown)
{
- // Asset handle cleanup is owned by ResourceExtComponent/ResourceService.
}
}
}
diff --git a/Runtime/UI/Constant/UIMetadataObject.cs b/Runtime/UI/Constant/UIMetadataObject.cs
index c635fd3..3446922 100644
--- a/Runtime/UI/Constant/UIMetadataObject.cs
+++ b/Runtime/UI/Constant/UIMetadataObject.cs
@@ -3,7 +3,7 @@ using AlicizaX.ObjectPool;
namespace AlicizaX.UI.Runtime
{
- internal class UIMetadataObject : ObjectBase
+ internal class UIMetadataObject : ObjectBase
{
public static UIMetadataObject Create(UIMetadata target, string name)
{
@@ -22,7 +22,7 @@ namespace AlicizaX.UI.Runtime
protected internal override void Release(bool isShutdown)
{
- UIMetadata metadata = (UIMetadata)Target;
+ UIMetadata metadata = Target;
if (metadata != null)
{
}
@@ -32,7 +32,7 @@ namespace AlicizaX.UI.Runtime
{
base.OnUnspawn();
- UIMetadata metadata = (UIMetadata)Target;
+ UIMetadata metadata = Target;
if (metadata != null)
{
metadata.CancelAsyncOperations();