|
|
|
@ -2,6 +2,7 @@ |
|
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
|
|
|
|
using System.Buffers; |
|
|
|
using System.Globalization; |
|
|
|
using System.Runtime.CompilerServices; |
|
|
|
using System.Runtime.InteropServices; |
|
|
|
using Microsoft.DotNet.RemoteExecutor; |
|
|
|
@ -273,67 +274,76 @@ public class UniformUnmanagedPoolMemoryAllocatorTests |
|
|
|
[InlineData(1200)] // Group of two UniformUnmanagedMemoryPool buffers
|
|
|
|
public void AllocateMemoryGroup_Finalization_ReturnsToPool(int length) |
|
|
|
{ |
|
|
|
if (TestEnvironment.IsMacOS) |
|
|
|
{ |
|
|
|
// Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (TestEnvironment.OSArchitecture == Architecture.Arm64) |
|
|
|
{ |
|
|
|
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (!TestEnvironment.RunsOnCI) |
|
|
|
{ |
|
|
|
// This may fail in local runs resulting in high memory load.
|
|
|
|
// Remove the condition for local debugging!
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// RunTest(length.ToString());
|
|
|
|
RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose(); |
|
|
|
RemoteExecutor.Invoke(RunTest, length.ToString(CultureInfo.InvariantCulture)).Dispose(); |
|
|
|
|
|
|
|
static void RunTest(string lengthStr) |
|
|
|
{ |
|
|
|
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(512, 1024, 16 * 1024, 1024); |
|
|
|
int lengthInner = int.Parse(lengthStr); |
|
|
|
|
|
|
|
int lengthInner = int.Parse(lengthStr, CultureInfo.InvariantCulture); |
|
|
|
|
|
|
|
// We want to verify that a leaked (not disposed) `MemoryGroup<byte>` still returns its
|
|
|
|
// unmanaged handles into the pool when it is finalized.
|
|
|
|
//
|
|
|
|
// We intentionally do NOT validate this by checking the contents of the re-rented memory
|
|
|
|
// (contents are not guaranteed to be preserved) nor by comparing pointer values
|
|
|
|
// (the pool may return a different handle while still correctly pooling).
|
|
|
|
//
|
|
|
|
// Instead, we validate that after a forced GC+finalization cycle, a subsequent allocation
|
|
|
|
// of the same size does not cause the number of outstanding unmanaged handles to *increase*
|
|
|
|
// compared to a known baseline.
|
|
|
|
|
|
|
|
// Establish a baseline: create one allocation and dispose it so the pool is initialized.
|
|
|
|
// (This ensures subsequent observations are not biased by first-time pool growth.)
|
|
|
|
allocator.AllocateGroup<byte>(lengthInner, 100).Dispose(); |
|
|
|
int baselineHandles = UnmanagedMemoryHandle.TotalOutstandingHandles; |
|
|
|
|
|
|
|
// Leak one allocation and force finalization.
|
|
|
|
AllocateGroupAndForget(allocator, lengthInner); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
|
|
|
|
AllocateGroupAndForget(allocator, lengthInner, true); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
|
|
|
|
using MemoryGroup<byte> g = allocator.AllocateGroup<byte>(lengthInner, 100); |
|
|
|
Assert.Equal(42, g.First().Span[0]); |
|
|
|
// Allocate again. If the leaked group was finalized correctly and returned to the pool,
|
|
|
|
// this should not require additional unmanaged allocations (ie, the handle count must not grow).
|
|
|
|
allocator.AllocateGroup<byte>(lengthInner, 100).Dispose(); |
|
|
|
|
|
|
|
// Note: we use "<=" instead of "==" here.
|
|
|
|
//
|
|
|
|
// After we record the baseline, the pool is allowed to legitimately *decrease*
|
|
|
|
// `UnmanagedMemoryHandle.TotalOutstandingHandles` by trimming retained buffers
|
|
|
|
// (eg. via the pool's trim timer/GC callbacks/high-pressure logic).
|
|
|
|
//
|
|
|
|
// What must not happen is the opposite: the leaked (non-disposed) group should be finalized
|
|
|
|
// and its handles returned to the pool such that allocating again does NOT require creating
|
|
|
|
// additional unmanaged handles. Therefore the only invariant we can reliably assert here is
|
|
|
|
// "no growth" relative to the baseline.
|
|
|
|
Assert.True(UnmanagedMemoryHandle.TotalOutstandingHandles <= baselineHandles); |
|
|
|
GC.KeepAlive(allocator); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false) |
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)] |
|
|
|
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length) |
|
|
|
{ |
|
|
|
// Allocate a group and drop the reference without disposing.
|
|
|
|
// The test relies on the group's finalizer to return the rented memory to the pool.
|
|
|
|
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(length, 100); |
|
|
|
if (check) |
|
|
|
{ |
|
|
|
Assert.Equal(42, g.First().Span[0]); |
|
|
|
} |
|
|
|
|
|
|
|
g.First().Span[0] = 42; |
|
|
|
// Touch the memory to ensure the buffer is actually materialized/usable.
|
|
|
|
g[0].Span[0] = 42; |
|
|
|
|
|
|
|
if (length < 512) |
|
|
|
{ |
|
|
|
// For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
|
|
|
|
// repeat rental to make sure per-core buckets are also utilized.
|
|
|
|
// For ArrayPool.Shared, the first rented array may be stored in TLS on the finalizer thread.
|
|
|
|
// Repeat rental to increase the chance that per-core buckets are involved when length
|
|
|
|
// is small and allocations go through ArrayPool.
|
|
|
|
MemoryGroup<byte> g1 = allocator.AllocateGroup<byte>(length, 100); |
|
|
|
g1.First().Span[0] = 42; |
|
|
|
g1[0].Span[0] = 42; |
|
|
|
g1 = null; |
|
|
|
} |
|
|
|
|
|
|
|
g = null; |
|
|
|
} |
|
|
|
|
|
|
|
[Theory] |
|
|
|
@ -341,69 +351,63 @@ public class UniformUnmanagedPoolMemoryAllocatorTests |
|
|
|
[InlineData(600)] // Group of single UniformUnmanagedMemoryPool buffer
|
|
|
|
public void AllocateSingleMemoryOwner_Finalization_ReturnsToPool(int length) |
|
|
|
{ |
|
|
|
if (TestEnvironment.IsMacOS) |
|
|
|
{ |
|
|
|
// Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (TestEnvironment.OSArchitecture == Architecture.Arm64) |
|
|
|
{ |
|
|
|
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (!TestEnvironment.RunsOnCI) |
|
|
|
{ |
|
|
|
// This may fail in local runs resulting in high memory load.
|
|
|
|
// Remove the condition for local debugging!
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// RunTest(length.ToString());
|
|
|
|
RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose(); |
|
|
|
RemoteExecutor.Invoke(RunTest, length.ToString(CultureInfo.InvariantCulture)).Dispose(); |
|
|
|
|
|
|
|
static void RunTest(string lengthStr) |
|
|
|
{ |
|
|
|
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(512, 1024, 16 * 1024, 1024); |
|
|
|
int lengthInner = int.Parse(lengthStr); |
|
|
|
|
|
|
|
int lengthInner = int.Parse(lengthStr, CultureInfo.InvariantCulture); |
|
|
|
|
|
|
|
// This test verifies pooling behavior when an `IMemoryOwner<byte>` is leaked (not disposed)
|
|
|
|
// and must be returned to the pool by finalization.
|
|
|
|
//
|
|
|
|
// We do NOT use a sentinel byte value to prove reuse because the contents of pooled buffers
|
|
|
|
// are not required to be preserved across rentals.
|
|
|
|
//
|
|
|
|
// Instead, we assert that after forcing GC+finalization, renting the same size again does not
|
|
|
|
// increase `UnmanagedMemoryHandle.TotalOutstandingHandles` above a baseline.
|
|
|
|
|
|
|
|
// Establish a baseline: allocate+dispose once so the pool has a chance to materialize/retain buffers.
|
|
|
|
allocator.Allocate<byte>(lengthInner).Dispose(); |
|
|
|
int baselineHandles = UnmanagedMemoryHandle.TotalOutstandingHandles; |
|
|
|
|
|
|
|
// Leak one allocation and force finalization.
|
|
|
|
AllocateSingleAndForget(allocator, lengthInner); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
|
|
|
|
AllocateSingleAndForget(allocator, lengthInner, true); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
GC.Collect(); |
|
|
|
GC.WaitForPendingFinalizers(); |
|
|
|
// Allocate again. If the leaked owner was finalized correctly and returned to the pool,
|
|
|
|
// this should not require additional unmanaged allocations (ie, the handle count must not grow).
|
|
|
|
allocator.Allocate<byte>(lengthInner).Dispose(); |
|
|
|
|
|
|
|
using IMemoryOwner<byte> g = allocator.Allocate<byte>(lengthInner); |
|
|
|
Assert.Equal(42, g.GetSpan()[0]); |
|
|
|
GC.KeepAlive(allocator); |
|
|
|
// Note: we use "<=" rather than "==". The pool may legitimately trim and free retained buffers,
|
|
|
|
// reducing the handle count between baseline and check. The invariant is "no growth".
|
|
|
|
Assert.True(UnmanagedMemoryHandle.TotalOutstandingHandles <= baselineHandles); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)] |
|
|
|
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false) |
|
|
|
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length) |
|
|
|
{ |
|
|
|
IMemoryOwner<byte> g = allocator.Allocate<byte>(length); |
|
|
|
if (check) |
|
|
|
{ |
|
|
|
Assert.Equal(42, g.GetSpan()[0]); |
|
|
|
} |
|
|
|
// Allocate and intentionally do not dispose.
|
|
|
|
IMemoryOwner<byte>? g = allocator.Allocate<byte>(length); |
|
|
|
|
|
|
|
// Touch the memory to ensure the buffer is actually materialized/usable.
|
|
|
|
g.GetSpan()[0] = 42; |
|
|
|
|
|
|
|
if (length < 512) |
|
|
|
{ |
|
|
|
// For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
|
|
|
|
// repeat rental to make sure per-core buckets are also utilized.
|
|
|
|
// For ArrayPool.Shared, the first rented array may be stored in TLS on the finalizer thread.
|
|
|
|
// Repeat rental to increase the chance that per-core buckets are involved when length
|
|
|
|
// is small and allocations go through ArrayPool.
|
|
|
|
IMemoryOwner<byte> g1 = allocator.Allocate<byte>(length); |
|
|
|
g1.GetSpan()[0] = 42; |
|
|
|
g1 = null; |
|
|
|
} |
|
|
|
|
|
|
|
g = null; |
|
|
|
} |
|
|
|
|
|
|
|
[Fact] |
|
|
|
|