diff --git a/csharp/Platform.Collections.Tests/ArrayTests.cs b/csharp/Platform.Collections.Tests/ArrayTests.cs index 1b9e67fb..5523e599 100644 --- a/csharp/Platform.Collections.Tests/ArrayTests.cs +++ b/csharp/Platform.Collections.Tests/ArrayTests.cs @@ -1,3 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Xunit; using Platform.Collections.Arrays; @@ -20,5 +26,157 @@ public void GetElementTest() Assert.False(array.TryGetElement(10, out element)); Assert.Equal(0, element); } + + [Fact] + public void ArrayPoolBasicFunctionalityTest() + { + var array1 = ArrayPool.Allocate(10); + Assert.Equal(10, array1.Length); + + ArrayPool.Free(array1); + + var array2 = ArrayPool.Allocate(10); + Assert.Equal(10, array2.Length); + + // Should reuse the freed array + Assert.Same(array1, array2); + } + + [Fact] + public void ArrayPoolMemoryLeakTest_UnboundedPoolGrowthFixed() + { + // This test verifies that the pool growth is now bounded (memory leak fixed) + var initialPoolCount = GetPoolCount(); + + // Allocate arrays of many different sizes + for (long i = 1; i <= 1000; i++) + { + var array = ArrayPool.Allocate(i); + ArrayPool.Free(array); + } + + var finalPoolCount = GetPoolCount(); + + // The pool should NOT have grown significantly (fix applied) + Assert.True(finalPoolCount <= ArrayPool.DefaultSizesAmount, + $"Pool grew from {initialPoolCount} to {finalPoolCount} entries, but should be limited to {ArrayPool.DefaultSizesAmount}"); + } + + [Fact] + public void ArrayPoolMemoryLeakTest_ArrayContentNotCleared() + { + // This test verifies that array contents ARE cleared when returned to pool (fix applied) + var array = ArrayPool.Allocate(5); + var testObject = new object(); + array[0] = testObject; + + ArrayPool.Free(array); + + // Get the same array back from pool + var reusedArray = ArrayPool.Allocate(5); + Assert.Same(array, reusedArray); + + // The old object reference should be cleared (memory leak fixed) + Assert.Null(reusedArray[0]); + } + + [Fact] + public void ArrayPoolMemoryLeakTest_PoolSizeLimited() + { + // This test verifies that pool size is now limited + var pool = new ArrayPool(10, 5); // max 5 different sizes + + // Allocate arrays of 10 different sizes + for (long i = 1; i <= 10; i++) + { + var array = pool.Allocate(i); + pool.Free(array); + } + + var poolCount = GetInstancePoolCount(pool); + + // Pool should be limited to 5 sizes maximum + Assert.True(poolCount <= 5, $"Pool has {poolCount} entries, should be limited to 5"); + } + + [Fact] + public void ArrayPoolMemoryLeakTest_ThreadStaticLeakage() + { + // This test demonstrates potential ThreadStatic memory leak + var initialThreadCount = GetActiveThreadCount(); + var tasks = new List(); + + // Create multiple threads that use ArrayPool + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(() => + { + // Each thread creates its own ArrayPool instance via ThreadStatic + var array = ArrayPool.Allocate(100); + ArrayPool.Free(array); + + // Clean up thread instance to prevent memory leak + ArrayPool.ClearThreadInstance(); + })); + } + + Task.WaitAll(tasks.ToArray()); + + // Force garbage collection to see if ThreadStatic instances are cleaned up + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // This test now demonstrates the fix for ThreadStatic cleanup + } + + [Fact] + public void ArrayPoolThreadStaticCleanupTest() + { + // This test verifies that ClearThreadInstance works + // Use a custom ArrayPool instance to test the clear functionality + var pool = new ArrayPool(10, 10); + + var array = pool.Allocate(15); + pool.Free(array); + + // Verify that the instance has entries + var poolCountBefore = GetInstancePoolCount(pool); + Assert.True(poolCountBefore > 0, $"Pool should have entries before cleanup, but had {poolCountBefore}"); + + pool.Clear(); + + var poolCountAfter = GetInstancePoolCount(pool); + Assert.Equal(0, poolCountAfter); + } + + private static int GetPoolCount() + { + // Use reflection to access the private _pool field to count entries + var threadInstanceProperty = typeof(ArrayPool).GetProperty("ThreadInstance", + BindingFlags.NonPublic | BindingFlags.Static); + var threadInstance = threadInstanceProperty.GetValue(null); + + var poolField = typeof(ArrayPool).GetField("_pool", + BindingFlags.NonPublic | BindingFlags.Instance); + var pool = poolField.GetValue(threadInstance) as IDictionary; + + return pool?.Count ?? 0; + } + + private static int GetInstancePoolCount(ArrayPool instance) + { + // Use reflection to access the private _pool field to count entries in a specific instance + var poolField = typeof(ArrayPool).GetField("_pool", + BindingFlags.NonPublic | BindingFlags.Instance); + var pool = poolField.GetValue(instance) as IDictionary; + + return pool?.Count ?? 0; + } + + private static int GetActiveThreadCount() + { + return Process.GetCurrentProcess().Threads.Count; + } } } diff --git a/csharp/Platform.Collections/Arrays/ArrayPool.cs b/csharp/Platform.Collections/Arrays/ArrayPool.cs index 724749c3..8c8ea952 100644 --- a/csharp/Platform.Collections/Arrays/ArrayPool.cs +++ b/csharp/Platform.Collections/Arrays/ArrayPool.cs @@ -44,5 +44,13 @@ public static class ArrayPool /// The array to be freed into the pull.Массив который нужно освобоить в пулл. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Free(T[] array) => ArrayPool.ThreadInstance.Free(array); + + /// + /// Clears the thread-static instance for the current thread to prevent memory leaks. + /// Очищает экземпляр ThreadStatic для текущего потока, чтобы предотвратить утечки памяти. + /// + /// The array elements type.Тип элементов массива. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ClearThreadInstance() => ArrayPool.ClearThreadInstance(); } } \ No newline at end of file diff --git a/csharp/Platform.Collections/Arrays/ArrayPool[T].cs b/csharp/Platform.Collections/Arrays/ArrayPool[T].cs index c1143e4b..efae6aac 100644 --- a/csharp/Platform.Collections/Arrays/ArrayPool[T].cs +++ b/csharp/Platform.Collections/Arrays/ArrayPool[T].cs @@ -33,7 +33,8 @@ public class ArrayPool /// internal static ArrayPool ThreadInstance => _threadInstance ?? (_threadInstance = new ArrayPool()); private readonly int _maxArraysPerSize; - private readonly Dictionary> _pool = new Dictionary>(ArrayPool.DefaultSizesAmount); + private readonly int _maxPoolSizes; + private readonly Dictionary> _pool; /// /// Initializes a new instance of the ArrayPool class using the specified maximum number of arrays per size. @@ -41,7 +42,21 @@ public class ArrayPool /// /// The maximum number of arrays in the pool per size.Максимальное количество массивов в пуле на каждый размер. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArrayPool(int maxArraysPerSize) => _maxArraysPerSize = maxArraysPerSize; + public ArrayPool(int maxArraysPerSize) : this(maxArraysPerSize, ArrayPool.DefaultSizesAmount) { } + + /// + /// Initializes a new instance of the ArrayPool class using the specified maximum number of arrays per size and maximum pool sizes. + /// Инициализирует новый экземпляр класса ArrayPool, используя указанное максимальное количество массивов на каждый размер и максимальное количество размеров пула. + /// + /// The maximum number of arrays in the pool per size.Максимальное количество массивов в пуле на каждый размер. + /// The maximum number of different sizes in the pool.Максимальное количество разных размеров в пуле. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArrayPool(int maxArraysPerSize, int maxPoolSizes) + { + _maxArraysPerSize = maxArraysPerSize; + _maxPoolSizes = maxPoolSizes; + _pool = new Dictionary>(maxPoolSizes); + } /// /// Initializes a new instance of the ArrayPool class using the default maximum number of arrays per size. @@ -93,6 +108,17 @@ public Disposable Resize(Disposable source, long size) [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual void Clear() => _pool.Clear(); + /// + /// Clears the thread-static instance for the current thread to prevent memory leaks. + /// Очищает экземпляр ThreadStatic для текущего потока, чтобы предотвратить утечки памяти. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ClearThreadInstance() + { + _threadInstance?.Clear(); + _threadInstance = null; + } + /// /// Retrieves an array with the specified size from the pool. /// Извлекает из пула массив с указанным размером. @@ -117,11 +143,25 @@ public virtual void Free(T[] array) { return; } + + // Check if we have too many different sizes, reject if pool is at capacity + if (!_pool.ContainsKey(array.LongLength) && _pool.Count >= _maxPoolSizes) + { + return; + } + var stack = _pool.GetOrAdd(array.LongLength, size => new Stack(_maxArraysPerSize)); if (stack.Count == _maxArraysPerSize) // Stack is full { return; } + + // Clear array contents to prevent memory leaks from object references + if (!typeof(T).IsValueType) + { + Array.Clear(array, 0, array.Length); + } + stack.Push(array); } }