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