// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. // ReSharper disable InconsistentNaming using System; using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32; using SixLabors.ImageSharp.Tests; using Xunit; namespace SixLabors.ImageSharp.Memory.Tests { public class ArrayPoolMemoryAllocatorTests { private const int MaxPooledBufferSizeInBytes = 2048; private const int PoolSelectorThresholdInBytes = MaxPooledBufferSizeInBytes / 2; /// /// Contains SUT for in-process tests. /// private MemoryAllocatorFixture LocalFixture { get; } = new MemoryAllocatorFixture(); /// /// Contains SUT for tests executed by , /// recreated in each external process. /// private static MemoryAllocatorFixture StaticFixture { get; } = new MemoryAllocatorFixture(); static ArrayPoolMemoryAllocatorTests() { TestEnvironment.PrepareRemoteExecutor(); } 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)) { firstAlloc.GetSpan().Fill(666); } using (IMemoryOwner secondAlloc = 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) { MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; IMemoryOwner buffer = memoryAllocator.Allocate(32); ref int ptrToPrev0 = ref MemoryMarshal.GetReference(buffer.GetSpan()); if (!keepBufferAlive) { buffer.Dispose(); } memoryAllocator.ReleaseRetainedResources(); buffer = memoryAllocator.Allocate(32); Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.GetReference())); } [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 small.GetReference(); small.Dispose(); IMemoryOwner large = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold + 1); Assert.False(Unsafe.AreSame(ref ptr2Small, ref large.GetReference())); } 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((int.MaxValue / SizeOfLargeStruct) + 1)] public void AllocateIncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) { ArgumentOutOfRangeException ex = Assert.Throws(() => this.LocalFixture.MemoryAllocator.Allocate(length)); Assert.Equal("length", ex.ParamName); } [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 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. /// public 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; } } [StructLayout(LayoutKind.Sequential)] private struct SmallStruct { private readonly uint dummy; } private const int SizeOfLargeStruct = MaxPooledBufferSizeInBytes / 5; [StructLayout(LayoutKind.Explicit, Size = SizeOfLargeStruct)] private struct LargeStruct { } } }