Skip to content

Commit

Permalink
Enable trimming TLS arrays in ArrayPool.Shared (#56316)
Browse files Browse the repository at this point in the history
* Enable trimming TLS arrays in ArrayPool.Shared

Today arrays stored in `ArrayPool<T>.Shared`'s per-core buckets have trimming applied, but arrays stored in the per-thread buckets are only trimmed when there's high memory pressure.  This change enables all buffers to be trimmed (eventually).  Every time our gen2 callback runs, it ensures any non-timestamped buffers have a timestamp, and also ensures that any timestamped buffers are still timely... if any aren't, they're eligible for trimming.  The timestamp is reset for TLS arrays when they're stored, and for per-core buckets when they transition from empty to non-empty; the latter is just a tweak on the current behavior, which incurs the cost of Environment.TickCount upon that transition, whereas now we only pay it as part of the trimming pass.

* Address PR feedback

* Work around bad linker transform
  • Loading branch information
stephentoub authored Jul 31, 2021
1 parent 65f04b9 commit 7d9a08c
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 99 deletions.
78 changes: 56 additions & 22 deletions src/libraries/System.Buffers/tests/ArrayPool/CollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,48 +23,51 @@ public void BuffersAreCollectedWhenStale()
const int BufferCount = 8;
const int BufferSize = 1025;

// Get the pool and check our trim setting
var pool = ArrayPool<int>.Shared;

List<int[]> rentedBuffers = new List<int[]>();

// Rent and return a set of buffers
for (int i = 0; i < BufferCount; i++)
{
rentedBuffers.Add(pool.Rent(BufferSize));
rentedBuffers.Add(ArrayPool<int>.Shared.Rent(BufferSize));
}
for (int i = 0; i < BufferCount; i++)
{
pool.Return(rentedBuffers[i]);
ArrayPool<int>.Shared.Return(rentedBuffers[i]);
}

// Rent what we returned and ensure they are the same
for (int i = 0; i < BufferCount; i++)
{
var buffer = pool.Rent(BufferSize);
var buffer = ArrayPool<int>.Shared.Rent(BufferSize);
Assert.Contains(rentedBuffers, item => ReferenceEquals(item, buffer));
}
for (int i = 0; i < BufferCount; i++)
{
pool.Return(rentedBuffers[i]);
ArrayPool<int>.Shared.Return(rentedBuffers[i]);
}

// Trigger a few Gen2 GCs to make sure the pool has appropriately time stamped buffers.
for (int i = 0; i < 2; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}

// Now wait a little over a minute and force a GC to get some buffers returned
Console.WriteLine("Waiting a minute for buffers to go stale...");
Thread.Sleep(61 * 1000);
GC.Collect(2);
GC.Collect();
GC.WaitForPendingFinalizers();
bool foundNewBuffer = false;
for (int i = 0; i < BufferCount; i++)
{
var buffer = pool.Rent(BufferSize);
var buffer = ArrayPool<int>.Shared.Rent(BufferSize);
if (!rentedBuffers.Any(item => ReferenceEquals(item, buffer)))
{
foundNewBuffer = true;
}
}

// Should only have found a new buffer if we're trimming
Assert.True(foundNewBuffer);
}, 3 * 60 * 1000); // This test has to wait for the buffers to go stale (give it three minutes)
}
Expand All @@ -78,21 +81,18 @@ public unsafe void ThreadLocalIsCollectedUnderHighPressure()
{
RemoteInvokeWithTrimming(() =>
{
// Get the pool and check our trim setting
var pool = ArrayPool<byte>.Shared;

// Create our buffer, return it, re-rent it and ensure we have the same one
const int BufferSize = 4097;
var buffer = pool.Rent(BufferSize);
pool.Return(buffer);
Assert.Same(buffer, pool.Rent(BufferSize));
byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
ArrayPool<byte>.Shared.Return(buffer);
Assert.Same(buffer, ArrayPool<byte>.Shared.Rent(BufferSize));

// Return it and put memory pressure on to get it cleared
pool.Return(buffer);
ArrayPool<byte>.Shared.Return(buffer);

const int AllocSize = 1024 * 1024 * 64;
int PageSize = Environment.SystemPageSize;
var pressureMethod = pool.GetType().GetMethod("GetMemoryPressure", BindingFlags.Static | BindingFlags.NonPublic);
var pressureMethod = ArrayPool<byte>.Shared.GetType().GetMethod("GetMemoryPressure", BindingFlags.Static | BindingFlags.NonPublic);
do
{
Span<byte> native = new Span<byte>(Marshal.AllocHGlobal(AllocSize).ToPointer(), AllocSize);
Expand All @@ -109,10 +109,45 @@ public unsafe void ThreadLocalIsCollectedUnderHighPressure()
GC.WaitForPendingFinalizers();

// Should have a new buffer now
Assert.NotSame(buffer, pool.Rent(BufferSize));
Assert.NotSame(buffer, ArrayPool<byte>.Shared.Rent(BufferSize));
});
}

// This test can cause problems for other tests run in parallel (from other assemblies) as
// it pushes the physical memory usage above 80% temporarily.
[OuterLoop("This is a long running test (over 2 minutes)")]
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public unsafe void ThreadLocalIsCollectedUnderNormalPressure()
{
RemoteInvokeWithTrimming(() =>
{
// Create our buffer, return it, re-rent it and ensure we have the same one
const int BufferSize = 4097;
byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
ArrayPool<byte>.Shared.Return(buffer);
Assert.Same(buffer, ArrayPool<byte>.Shared.Rent(BufferSize));

// Return it and put memory pressure on to get it cleared
ArrayPool<byte>.Shared.Return(buffer);

// Make sure buffer gets time stamped
for (int i = 0; i < 2; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}

// Now wait for enough time to pass and force a GC to get buffers dropped
Console.WriteLine("Waiting a minute for buffers to go stale...");
Thread.Sleep(61 * 1000);
GC.Collect();
GC.WaitForPendingFinalizers();

// Should have a new buffer now
Assert.NotSame(buffer, ArrayPool<byte>.Shared.Rent(BufferSize));
}, 3 * 60 * 1000); // This test has to wait for the buffers to go stale (give it three minutes)
}

private static bool IsPreciseGcSupportedAndRemoteExecutorSupported => PlatformDetection.IsPreciseGcSupported && RemoteExecutor.IsSupported;

[ActiveIssue("https://github.com/dotnet/runtime/issues/44037")]
Expand All @@ -121,9 +156,8 @@ public void PollingEventFires()
{
RemoteInvokeWithTrimming(() =>
{
var pool = ArrayPool<float>.Shared;
bool pollEventFired = false;
var buffer = pool.Rent(10);
float[] buffer = ArrayPool<float>.Shared.Rent(10);

// Polling doesn't start until the thread locals are created for a pool.
// Try before the return then after.
Expand All @@ -141,7 +175,7 @@ public void PollingEventFires()
});

Assert.False(pollEventFired, "collection isn't hooked up until the first item is returned");
pool.Return(buffer);
ArrayPool<float>.Shared.Return(buffer);

RunWithListener(() =>
{
Expand Down
Loading

0 comments on commit 7d9a08c

Please sign in to comment.