// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. // ReSharper disable InconsistentNaming using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Tests; using Xunit; namespace SixLabors.Memory.Tests { // TODO: Re-enable memory-intensive tests with arcade RemoteExecutor: // https://github.com/dotnet/runtime/blob/master/docs/project/writing-tests.md#remoteexecutor public class ArrayPoolMemoryAllocatorTests { private const int MaxPooledBufferSizeInBytes = 2048; private const int PoolSelectorThresholdInBytes = MaxPooledBufferSizeInBytes / 2; private MemoryAllocator MemoryAllocator { get; set; } = new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes); /// /// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location. /// private bool CheckIsRentingPooledBuffer(int length) where T : struct { IMemoryOwner buffer = this.MemoryAllocator.Allocate(length); ref T ptrToPrevPosition0 = ref buffer.GetReference(); buffer.Dispose(); buffer = this.MemoryAllocator.Allocate(length); bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref buffer.GetReference()); buffer.Dispose(); return sameBuffers; } 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.CheckIsRentingPooledBuffer(size)); } [Theory(Skip = "Should be executed from a separate process.")] [InlineData(128 * 1024 * 1024)] [InlineData(MaxPooledBufferSizeInBytes + 1)] public void LargeBuffersAreNotPooled_OfByte(int size) { if (!TestEnvironment.Is64BitProcess) { // can lead to OutOfMemoryException return; } Assert.False(this.CheckIsRentingPooledBuffer(size)); } [Fact] public unsafe void SmallBuffersArePooled_OfBigValueType() { int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) - 1; Assert.True(this.CheckIsRentingPooledBuffer(count)); } [Fact(Skip = "Should be executed from a separate process.")] public unsafe void LaregeBuffersAreNotPooled_OfBigValueType() { if (!TestEnvironment.Is64BitProcess) { // can lead to OutOfMemoryException return; } int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) + 1; Assert.False(this.CheckIsRentingPooledBuffer(count)); } [Theory] [InlineData(AllocationOptions.None)] [InlineData(AllocationOptions.Clean)] public void CleaningRequests_AreControlledByAllocationParameter_Clean(AllocationOptions options) { using (IMemoryOwner firstAlloc = this.MemoryAllocator.Allocate(42)) { firstAlloc.GetSpan().Fill(666); } using (IMemoryOwner secondAlloc = this.MemoryAllocator.Allocate(42, options)) { int expected = options == AllocationOptions.Clean ? 0 : 666; Assert.Equal(expected, secondAlloc.GetSpan()[0]); } } [Theory] [InlineData(false)] [InlineData(true)] public void ReleaseRetainedResources_ReplacesInnerArrayPool(bool keepBufferAlive) { IMemoryOwner buffer = this.MemoryAllocator.Allocate(32); ref int ptrToPrev0 = ref MemoryMarshal.GetReference(buffer.GetSpan()); if (!keepBufferAlive) { buffer.Dispose(); } this.MemoryAllocator.ReleaseRetainedResources(); buffer = this.MemoryAllocator.Allocate(32); Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.GetReference())); } [Fact] public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed() { IMemoryOwner buffer = this.MemoryAllocator.Allocate(32); this.MemoryAllocator.ReleaseRetainedResources(); buffer.Dispose(); } [Fact(Skip = "Should be executed from a separate process.")] public void AllocationOverLargeArrayThreshold_UsesDifferentPool() { if (!TestEnvironment.Is64BitProcess) { // can lead to OutOfMemoryException return; } const int ArrayLengthThreshold = PoolSelectorThresholdInBytes / sizeof(int); IMemoryOwner small = this.MemoryAllocator.Allocate(ArrayLengthThreshold - 1); ref int ptr2Small = ref small.GetReference(); small.Dispose(); IMemoryOwner large = this.MemoryAllocator.Allocate(ArrayLengthThreshold + 1); Assert.False(Unsafe.AreSame(ref ptr2Small, ref large.GetReference())); } [Fact(Skip = "Should be executed from a separate process.")] public void CreateWithAggressivePooling() { if (!TestEnvironment.Is64BitProcess) { // can lead to OutOfMemoryException return; } this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithAggressivePooling(); Assert.True(this.CheckIsRentingPooledBuffer(4096 * 4096)); } [Fact(Skip = "Should be executed from a separate process.")] public void CreateDefault() { if (!TestEnvironment.Is64BitProcess) { // can lead to OutOfMemoryException return; } this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault(); Assert.False(this.CheckIsRentingPooledBuffer(2 * 4096 * 4096)); Assert.True(this.CheckIsRentingPooledBuffer(2048 * 2048)); } [Fact] public void CreateWithModeratePooling() { if (!TestEnvironment.Is64BitProcess) { // can lead to OutOfMemoryException return; } this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling(); Assert.False(this.CheckIsRentingPooledBuffer(2048 * 2048)); Assert.True(this.CheckIsRentingPooledBuffer(1024 * 16)); } [StructLayout(LayoutKind.Sequential)] private struct Rgba32 { private readonly uint dummy; } private const int SizeOfLargeStruct = MaxPooledBufferSizeInBytes / 5; [StructLayout(LayoutKind.Explicit, Size = SizeOfLargeStruct)] private struct LargeStruct { } [Theory] [InlineData(-1)] [InlineData((int.MaxValue / SizeOfLargeStruct) + 1)] public void AllocateIncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) { ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length)); Assert.Equal("length", ex.ParamName); } [Theory] [InlineData(-1)] public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) { ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.AllocateManagedByteBuffer(length)); Assert.Equal("length", ex.ParamName); } } }