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();