Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions csharp/Platform.Collections.Tests/ArrayTests.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<int>(10);
Assert.Equal(10, array1.Length);

ArrayPool.Free(array1);

var array2 = ArrayPool.Allocate<int>(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<object>();

// Allocate arrays of many different sizes
for (long i = 1; i <= 1000; i++)
{
var array = ArrayPool.Allocate<object>(i);
ArrayPool.Free(array);
}

var finalPoolCount = GetPoolCount<object>();

// 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<object>(5);
var testObject = new object();
array[0] = testObject;

ArrayPool.Free(array);

// Get the same array back from pool
var reusedArray = ArrayPool.Allocate<object>(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<object>(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<Task>();

// 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<object>(100);
ArrayPool.Free(array);

// Clean up thread instance to prevent memory leak
ArrayPool.ClearThreadInstance<object>();
}));
}

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<string>(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<T>()
{
// Use reflection to access the private _pool field to count entries
var threadInstanceProperty = typeof(ArrayPool<T>).GetProperty("ThreadInstance",
BindingFlags.NonPublic | BindingFlags.Static);
var threadInstance = threadInstanceProperty.GetValue(null);

var poolField = typeof(ArrayPool<T>).GetField("_pool",
BindingFlags.NonPublic | BindingFlags.Instance);
var pool = poolField.GetValue(threadInstance) as IDictionary<long, object>;

return pool?.Count ?? 0;
}

private static int GetInstancePoolCount<T>(ArrayPool<T> instance)
{
// Use reflection to access the private _pool field to count entries in a specific instance
var poolField = typeof(ArrayPool<T>).GetField("_pool",
BindingFlags.NonPublic | BindingFlags.Instance);
var pool = poolField.GetValue(instance) as IDictionary<long, object>;

return pool?.Count ?? 0;
}

private static int GetActiveThreadCount()
{
return Process.GetCurrentProcess().Threads.Count;
}
}
}
8 changes: 8 additions & 0 deletions csharp/Platform.Collections/Arrays/ArrayPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,13 @@ public static class ArrayPool
/// <param name="array"><para>The array to be freed into the pull.</para><para>Массив который нужно освобоить в пулл.</para></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Free<T>(T[] array) => ArrayPool<T>.ThreadInstance.Free(array);

/// <summary>
/// <para>Clears the thread-static instance for the current thread to prevent memory leaks.</para>
/// <para>Очищает экземпляр ThreadStatic для текущего потока, чтобы предотвратить утечки памяти.</para>
/// </summary>
/// <typeparam name="T"><para>The array elements type.</para><para>Тип элементов массива.</para></typeparam>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClearThreadInstance<T>() => ArrayPool<T>.ClearThreadInstance();
}
}
44 changes: 42 additions & 2 deletions csharp/Platform.Collections/Arrays/ArrayPool[T].cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,30 @@ public class ArrayPool<T>
/// </summary>
internal static ArrayPool<T> ThreadInstance => _threadInstance ?? (_threadInstance = new ArrayPool<T>());
private readonly int _maxArraysPerSize;
private readonly Dictionary<long, Stack<T[]>> _pool = new Dictionary<long, Stack<T[]>>(ArrayPool.DefaultSizesAmount);
private readonly int _maxPoolSizes;
private readonly Dictionary<long, Stack<T[]>> _pool;

/// <summary>
/// <para>Initializes a new instance of the ArrayPool class using the specified maximum number of arrays per size.</para>
/// <para>Инициализирует новый экземпляр класса ArrayPool, используя указанное максимальное количество массивов на каждый размер.</para>
/// </summary>
/// <param name="maxArraysPerSize"><para>The maximum number of arrays in the pool per size.</para><para>Максимальное количество массивов в пуле на каждый размер.</para></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArrayPool(int maxArraysPerSize) => _maxArraysPerSize = maxArraysPerSize;
public ArrayPool(int maxArraysPerSize) : this(maxArraysPerSize, ArrayPool.DefaultSizesAmount) { }

/// <summary>
/// <para>Initializes a new instance of the ArrayPool class using the specified maximum number of arrays per size and maximum pool sizes.</para>
/// <para>Инициализирует новый экземпляр класса ArrayPool, используя указанное максимальное количество массивов на каждый размер и максимальное количество размеров пула.</para>
/// </summary>
/// <param name="maxArraysPerSize"><para>The maximum number of arrays in the pool per size.</para><para>Максимальное количество массивов в пуле на каждый размер.</para></param>
/// <param name="maxPoolSizes"><para>The maximum number of different sizes in the pool.</para><para>Максимальное количество разных размеров в пуле.</para></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArrayPool(int maxArraysPerSize, int maxPoolSizes)
{
_maxArraysPerSize = maxArraysPerSize;
_maxPoolSizes = maxPoolSizes;
_pool = new Dictionary<long, Stack<T[]>>(maxPoolSizes);
}

/// <summary>
/// <para>Initializes a new instance of the ArrayPool class using the default maximum number of arrays per size.</para>
Expand Down Expand Up @@ -93,6 +108,17 @@ public Disposable<T[]> Resize(Disposable<T[]> source, long size)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Clear() => _pool.Clear();

/// <summary>
/// <para>Clears the thread-static instance for the current thread to prevent memory leaks.</para>
/// <para>Очищает экземпляр ThreadStatic для текущего потока, чтобы предотвратить утечки памяти.</para>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClearThreadInstance()
{
_threadInstance?.Clear();
_threadInstance = null;
}

/// <summary>
/// <para>Retrieves an array with the specified size from the pool.</para>
/// <para>Извлекает из пула массив с указанным размером.</para>
Expand All @@ -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<T[]>(_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);
}
}
Expand Down
Loading