// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.Memory; using Xunit; namespace SixLabors.ImageSharp.Tests.Memory.Allocators { [Collection("RunSerial")] public class ArrayPoolMemoryAllocatorTests { private const int MaxPooledBufferSizeInBytes = 2048; private const int PoolSelectorThresholdInBytes = MaxPooledBufferSizeInBytes / 2; /// /// Gets the SUT for in-process tests. /// private MemoryAllocatorFixture LocalFixture { get; } = new MemoryAllocatorFixture(); /// /// Gets the SUT for tests executed by , /// recreated in each external process. /// private static MemoryAllocatorFixture StaticFixture { get; } = new MemoryAllocatorFixture(); public class BufferTests : BufferTestSuite { public BufferTests() : base(new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes)) { } } public class Constructor { [Fact] public void WhenBothParametersPassedByUser() { var mgr = new ArrayPoolMemoryAllocator(1111, 666); Assert.Equal(1111, mgr.MaxPoolSizeInBytes); Assert.Equal(666, mgr.PoolSelectorThresholdInBytes); } [Fact] public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdValueIsAutoCalculated() { var mgr = new ArrayPoolMemoryAllocator(5000); Assert.Equal(5000, mgr.MaxPoolSizeInBytes); Assert.True(mgr.PoolSelectorThresholdInBytes < mgr.MaxPoolSizeInBytes); } [Fact] public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown() => Assert.ThrowsAny(() => new ArrayPoolMemoryAllocator(100, 200)); } [Theory] [InlineData(32)] [InlineData(512)] [InlineData(MaxPooledBufferSizeInBytes - 1)] public void SmallBuffersArePooled_OfByte(int size) => Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer(size)); [Theory] [InlineData(128 * 1024 * 1024)] [InlineData(MaxPooledBufferSizeInBytes + 1)] public void LargeBuffersAreNotPooled_OfByte(int size) { static void RunTest(string sizeStr) { int size = int.Parse(sizeStr); StaticFixture.CheckIsRentingPooledBuffer(size); } RemoteExecutor.Invoke(RunTest, size.ToString()).Dispose(); } [Fact] public unsafe void SmallBuffersArePooled_OfBigValueType() { int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) - 1; Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer(count)); } [Fact] public unsafe void LaregeBuffersAreNotPooled_OfBigValueType() { int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) + 1; Assert.False(this.LocalFixture.CheckIsRentingPooledBuffer(count)); } [Theory] [InlineData(AllocationOptions.None)] [InlineData(AllocationOptions.Clean)] public void CleaningRequests_AreControlledByAllocationParameter_Clean(AllocationOptions options) { MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; using (IMemoryOwner firstAlloc = memoryAllocator.Allocate(42)) { BufferExtensions.GetSpan(firstAlloc).Fill(666); } using (IMemoryOwner secondAlloc = memoryAllocator.Allocate(42, options)) { int expected = options == AllocationOptions.Clean ? 0 : 666; Assert.Equal(expected, BufferExtensions.GetSpan(secondAlloc)[0]); } } [Fact] public unsafe void Allocate_MemoryIsPinnableMultipleTimes() { ArrayPoolMemoryAllocator allocator = this.LocalFixture.MemoryAllocator; using IMemoryOwner memoryOwner = allocator.Allocate(100); using (MemoryHandle pin = memoryOwner.Memory.Pin()) { Assert.NotEqual(IntPtr.Zero, (IntPtr)pin.Pointer); } using (MemoryHandle pin = memoryOwner.Memory.Pin()) { Assert.NotEqual(IntPtr.Zero, (IntPtr)pin.Pointer); } } [Theory] [InlineData(false)] [InlineData(true)] public void ReleaseRetainedResources_ReplacesInnerArrayPool(bool keepBufferAlive) { MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; IMemoryOwner buffer = memoryAllocator.Allocate(32); ref int ptrToPrev0 = ref MemoryMarshal.GetReference(BufferExtensions.GetSpan(buffer)); if (!keepBufferAlive) { buffer.Dispose(); } memoryAllocator.ReleaseRetainedResources(); buffer = memoryAllocator.Allocate(32); Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref BufferExtensions.GetReference(buffer))); } [Fact] public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed() { MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; IMemoryOwner buffer = memoryAllocator.Allocate(32); memoryAllocator.ReleaseRetainedResources(); buffer.Dispose(); } [Fact] public void AllocationOverLargeArrayThreshold_UsesDifferentPool() { static void RunTest() { const int ArrayLengthThreshold = PoolSelectorThresholdInBytes / sizeof(int); IMemoryOwner small = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold - 1); ref int ptr2Small = ref BufferExtensions.GetReference(small); small.Dispose(); IMemoryOwner large = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold + 1); Assert.False(Unsafe.AreSame(ref ptr2Small, ref BufferExtensions.GetReference(large))); } RemoteExecutor.Invoke(RunTest).Dispose(); } [Fact] public void CreateWithAggressivePooling() { static void RunTest() { StaticFixture.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithAggressivePooling(); Assert.True(StaticFixture.CheckIsRentingPooledBuffer(4096 * 4096)); } RemoteExecutor.Invoke(RunTest).Dispose(); } [Fact] public void CreateDefault() { static void RunTest() { StaticFixture.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault(); Assert.False(StaticFixture.CheckIsRentingPooledBuffer(2 * 4096 * 4096)); Assert.True(StaticFixture.CheckIsRentingPooledBuffer(2048 * 2048)); } RemoteExecutor.Invoke(RunTest).Dispose(); } [Fact] public void CreateWithModeratePooling() { static void RunTest() { StaticFixture.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling(); Assert.False(StaticFixture.CheckIsRentingPooledBuffer(2048 * 2048)); Assert.True(StaticFixture.CheckIsRentingPooledBuffer(1024 * 16)); } RemoteExecutor.Invoke(RunTest).Dispose(); } [Theory] [InlineData(-1)] [InlineData(-111)] public void Allocate_Negative_Throws_ArgumentOutOfRangeException(int length) { ArgumentOutOfRangeException ex = Assert.Throws(() => this.LocalFixture.MemoryAllocator.Allocate(length)); Assert.Equal("length", ex.ParamName); } [Fact] public void AllocateZero() { using IMemoryOwner buffer = this.LocalFixture.MemoryAllocator.Allocate(0); Assert.Equal(0, buffer.Memory.Length); } [Theory] [InlineData(-1)] public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) { ArgumentOutOfRangeException ex = Assert.Throws(() => this.LocalFixture.MemoryAllocator.AllocateManagedByteBuffer(length)); Assert.Equal("length", ex.ParamName); } private class MemoryAllocatorFixture { public ArrayPoolMemoryAllocator MemoryAllocator { get; set; } = new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes); /// /// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location. /// public bool CheckIsRentingPooledBuffer(int length) where T : struct { IMemoryOwner buffer = this.MemoryAllocator.Allocate(length); ref T ptrToPrevPosition0 = ref BufferExtensions.GetReference(buffer); buffer.Dispose(); buffer = this.MemoryAllocator.Allocate(length); bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref BufferExtensions.GetReference(buffer)); buffer.Dispose(); return sameBuffers; } } [StructLayout(LayoutKind.Sequential)] private struct SmallStruct { private readonly uint dummy; } private const int SizeOfLargeStruct = MaxPooledBufferSizeInBytes / 5; [StructLayout(LayoutKind.Explicit, Size = SizeOfLargeStruct)] private struct LargeStruct { } } }