From 9f51d65d950fa691ec70eeb129aad5db35c49fee Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Thu, 26 Jul 2018 01:15:12 +0200 Subject: [PATCH] [SL.Core] Add MemoryAllocator-s with tests --- SixLabors.Core.sln.DotSettings | 3 +- .../Memory/AllocationOptions.cs | 21 ++ .../ArrayPoolMemoryAllocator.Buffer{T}.cs | 84 +++++ ...oolMemoryAllocator.CommonFactoryMethods.cs | 72 ++++ .../Memory/ArrayPoolMemoryAllocator.cs | 140 ++++++++ src/SixLabors.Core/Memory/BasicArrayBuffer.cs | 78 +++++ src/SixLabors.Core/Memory/BasicByteBuffer.cs | 20 ++ .../Memory/IManagedByteBuffer.cs | 18 + .../Memory/ManagedBufferBase.cs | 45 +++ src/SixLabors.Core/Memory/MemoryAllocator.cs | 39 +++ .../Memory/SimpleGcMemoryAllocator.cs | 25 ++ src/SixLabors.Core/SixLabors.Core.csproj | 3 + .../Memory/ArrayPoolMemoryManagerTests.cs | 240 +++++++++++++ .../Memory/BufferExtensions.cs | 25 ++ .../Memory/BufferTestSuite.cs | 318 ++++++++++++++++++ .../Memory/SimpleGcMemoryManagerTests.cs | 16 + .../SixLabors.Core.Tests.csproj | 1 + tests/SixLabors.Core.Tests/TestEnvironment.cs | 12 + 18 files changed, 1159 insertions(+), 1 deletion(-) create mode 100644 src/SixLabors.Core/Memory/AllocationOptions.cs create mode 100644 src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs create mode 100644 src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs create mode 100644 src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs create mode 100644 src/SixLabors.Core/Memory/BasicArrayBuffer.cs create mode 100644 src/SixLabors.Core/Memory/BasicByteBuffer.cs create mode 100644 src/SixLabors.Core/Memory/IManagedByteBuffer.cs create mode 100644 src/SixLabors.Core/Memory/ManagedBufferBase.cs create mode 100644 src/SixLabors.Core/Memory/MemoryAllocator.cs create mode 100644 src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs create mode 100644 tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryManagerTests.cs create mode 100644 tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs create mode 100644 tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs create mode 100644 tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryManagerTests.cs create mode 100644 tests/SixLabors.Core.Tests/TestEnvironment.cs diff --git a/SixLabors.Core.sln.DotSettings b/SixLabors.Core.sln.DotSettings index 613ba3882..82961f0d0 100644 --- a/SixLabors.Core.sln.DotSettings +++ b/SixLabors.Core.sln.DotSettings @@ -2,4 +2,5 @@ DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW - DO_NOT_SHOW \ No newline at end of file + DO_NOT_SHOW + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/AllocationOptions.cs b/src/SixLabors.Core/Memory/AllocationOptions.cs new file mode 100644 index 000000000..5eda00505 --- /dev/null +++ b/src/SixLabors.Core/Memory/AllocationOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory +{ + /// + /// Options for allocating buffers. + /// + public enum AllocationOptions + { + /// + /// Indicates that the buffer should just be allocated. + /// + None, + + /// + /// Indicates that the allocated buffer should be cleaned following allocation. + /// + Clean + } +} diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs new file mode 100644 index 000000000..b563d4a79 --- /dev/null +++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace SixLabors.Memory +{ + /// + /// Contains and + /// + public partial class ArrayPoolMemoryAllocator + { + /// + /// The buffer implementation of . + /// + private class Buffer : ManagedBufferBase + where T : struct + { + /// + /// The length of the buffer + /// + private readonly int length; + + /// + /// A weak reference to the source pool. + /// + /// + /// By using a weak reference here, we are making sure that array pools and their retained arrays are always GC-ed + /// after a call to , regardless of having buffer instances still being in use. + /// + private WeakReference> sourcePoolReference; + + public Buffer(byte[] data, int length, ArrayPool sourcePool) + { + this.Data = data; + this.length = length; + this.sourcePoolReference = new WeakReference>(sourcePool); + } + + /// + /// Gets the buffer as a byte array. + /// + protected byte[] Data { get; private set; } + + /// + public override Span GetSpan() => MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length); + + /// + protected override void Dispose(bool disposing) + { + if (!disposing || this.Data == null || this.sourcePoolReference == null) + { + return; + } + + if (this.sourcePoolReference.TryGetTarget(out ArrayPool pool)) + { + pool.Return(this.Data); + } + + this.sourcePoolReference = null; + this.Data = null; + } + + protected override object GetPinnableObject() => this.Data; + } + + /// + /// The implementation of . + /// + private class ManagedByteBuffer : Buffer, IManagedByteBuffer + { + public ManagedByteBuffer(byte[] data, int length, ArrayPool sourcePool) + : base(data, length, sourcePool) + { + } + + /// + public byte[] Array => this.Data; + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs new file mode 100644 index 000000000..a82948ebc --- /dev/null +++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory +{ + /// + /// Contains common factory methods and configuration constants. + /// + public partial class ArrayPoolMemoryAllocator + { + /// + /// The default value for: maximum size of pooled arrays in bytes. + /// Currently set to 24MB, which is equivalent to 8 megapixels of raw RGBA32 data. + /// + internal const int DefaultMaxPooledBufferSizeInBytes = 24 * 1024 * 1024; + + /// + /// The value for: The threshold to pool arrays in which has less buckets for memory safety. + /// + private const int DefaultBufferSelectorThresholdInBytes = 8 * 1024 * 1024; + + /// + /// The default bucket count for . + /// + private const int DefaultLargePoolBucketCount = 6; + + /// + /// The default bucket count for . + /// + private const int DefaultNormalPoolBucketCount = 16; + + /// + /// This is the default. Should be good for most use cases. + /// + /// The memory manager + public static ArrayPoolMemoryAllocator CreateDefault() + { + return new ArrayPoolMemoryAllocator( + DefaultMaxPooledBufferSizeInBytes, + DefaultBufferSelectorThresholdInBytes, + DefaultLargePoolBucketCount, + DefaultNormalPoolBucketCount); + } + + /// + /// For environments with limited memory capabilities. Only small images are pooled, which can result in reduced througput. + /// + /// The memory manager + public static ArrayPoolMemoryAllocator CreateWithModeratePooling() + { + return new ArrayPoolMemoryAllocator(1024 * 1024, 32 * 1024, 16, 24); + } + + /// + /// Only pool small buffers like image rows. + /// + /// The memory manager + public static ArrayPoolMemoryAllocator CreateWithMinimalPooling() + { + return new ArrayPoolMemoryAllocator(64 * 1024, 32 * 1024, 8, 24); + } + + /// + /// RAM is not an issue for me, gimme maximum througput! + /// + /// The memory manager + public static ArrayPoolMemoryAllocator CreateWithAggressivePooling() + { + return new ArrayPoolMemoryAllocator(128 * 1024 * 1024, 32 * 1024 * 1024, 16, 32); + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs new file mode 100644 index 000000000..8681a594b --- /dev/null +++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace SixLabors.Memory +{ + /// + /// Implements by allocating memory from . + /// + public sealed partial class ArrayPoolMemoryAllocator : MemoryAllocator + { + private readonly int maxArraysPerBucketNormalPool; + + private readonly int maxArraysPerBucketLargePool; + + /// + /// The for small-to-medium buffers which is not kept clean. + /// + private ArrayPool normalArrayPool; + + /// + /// The for huge buffers, which is not kept clean. + /// + private ArrayPool largeArrayPool; + + /// + /// Initializes a new instance of the class. + /// + public ArrayPoolMemoryAllocator() + : this(DefaultMaxPooledBufferSizeInBytes, DefaultBufferSelectorThresholdInBytes) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes) + : this(maxPoolSizeInBytes, GetLargeBufferThresholdInBytes(maxPoolSizeInBytes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + /// Arrays over this threshold will be pooled in which has less buckets for memory safety. + public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes) + : this(maxPoolSizeInBytes, poolSelectorThresholdInBytes, DefaultLargePoolBucketCount, DefaultNormalPoolBucketCount) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + /// The threshold to pool arrays in which has less buckets for memory safety. + /// Max arrays per bucket for the large array pool + /// Max arrays per bucket for the normal array pool + public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes, int maxArraysPerBucketLargePool, int maxArraysPerBucketNormalPool) + { + Guard.MustBeGreaterThan(maxPoolSizeInBytes, 0, nameof(maxPoolSizeInBytes)); + Guard.MustBeLessThanOrEqualTo(poolSelectorThresholdInBytes, maxPoolSizeInBytes, nameof(poolSelectorThresholdInBytes)); + + this.MaxPoolSizeInBytes = maxPoolSizeInBytes; + this.PoolSelectorThresholdInBytes = poolSelectorThresholdInBytes; + this.maxArraysPerBucketLargePool = maxArraysPerBucketLargePool; + this.maxArraysPerBucketNormalPool = maxArraysPerBucketNormalPool; + + this.InitArrayPools(); + } + + /// + /// Gets the maximum size of pooled arrays in bytes. + /// + public int MaxPoolSizeInBytes { get; } + + /// + /// Gets the threshold to pool arrays in which has less buckets for memory safety. + /// + public int PoolSelectorThresholdInBytes { get; } + + /// + public override void ReleaseRetainedResources() + { + this.InitArrayPools(); + } + + /// + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + { + int itemSizeBytes = Unsafe.SizeOf(); + int bufferSizeInBytes = length * itemSizeBytes; + + ArrayPool pool = this.GetArrayPool(bufferSizeInBytes); + byte[] byteArray = pool.Rent(bufferSizeInBytes); + + var buffer = new Buffer(byteArray, length, pool); + if (options == AllocationOptions.Clean) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + /// + public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) + { + ArrayPool pool = this.GetArrayPool(length); + byte[] byteArray = pool.Rent(length); + + var buffer = new ManagedByteBuffer(byteArray, length, pool); + if (options == AllocationOptions.Clean) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + private static int GetLargeBufferThresholdInBytes(int maxPoolSizeInBytes) + { + return maxPoolSizeInBytes / 4; + } + + private ArrayPool GetArrayPool(int bufferSizeInBytes) + { + return bufferSizeInBytes <= this.PoolSelectorThresholdInBytes ? this.normalArrayPool : this.largeArrayPool; + } + + private void InitArrayPools() + { + this.largeArrayPool = ArrayPool.Create(this.MaxPoolSizeInBytes, this.maxArraysPerBucketLargePool); + this.normalArrayPool = ArrayPool.Create(this.PoolSelectorThresholdInBytes, this.maxArraysPerBucketNormalPool); + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/BasicArrayBuffer.cs b/src/SixLabors.Core/Memory/BasicArrayBuffer.cs new file mode 100644 index 000000000..a3e2d02cc --- /dev/null +++ b/src/SixLabors.Core/Memory/BasicArrayBuffer.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.Memory +{ + /// + /// Wraps an array as an instance. + /// + /// + internal class BasicArrayBuffer : ManagedBufferBase + where T : struct + { + /// + /// Initializes a new instance of the class + /// + /// The array + /// The length of the buffer + public BasicArrayBuffer(T[] array, int length) + { + DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length)); + this.Array = array; + this.Length = length; + } + + /// + /// Initializes a new instance of the class + /// + /// The array + public BasicArrayBuffer(T[] array) + : this(array, array.Length) + { + } + + /// + /// Gets the array + /// + public T[] Array { get; } + + /// + /// Gets the length + /// + public int Length { get; } + + /// + /// Returns a reference to specified element of the buffer. + /// + /// The index + /// The reference to the specified element + public ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeLessThan(index, this.Length, nameof(index)); + + Span span = this.GetSpan(); + return ref span[index]; + } + } + + /// + public override Span GetSpan() => this.Array.AsSpan(0, this.Length); + + /// + protected override void Dispose(bool disposing) + { + } + + /// + protected override object GetPinnableObject() + { + return this.Array; + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/BasicByteBuffer.cs b/src/SixLabors.Core/Memory/BasicByteBuffer.cs new file mode 100644 index 000000000..5706ca87a --- /dev/null +++ b/src/SixLabors.Core/Memory/BasicByteBuffer.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory +{ + /// + /// Provides an based on . + /// + internal sealed class BasicByteBuffer : BasicArrayBuffer, IManagedByteBuffer + { + /// + /// Initializes a new instance of the class + /// + /// The byte array + internal BasicByteBuffer(byte[] array) + : base(array) + { + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/IManagedByteBuffer.cs b/src/SixLabors.Core/Memory/IManagedByteBuffer.cs new file mode 100644 index 000000000..b6d956c10 --- /dev/null +++ b/src/SixLabors.Core/Memory/IManagedByteBuffer.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.Memory +{ + /// + /// Represents a byte buffer backed by a managed array. Useful for interop with classic .NET API-s. + /// + public interface IManagedByteBuffer : IMemoryOwner + { + /// + /// Gets the managed array backing this buffer instance. + /// + byte[] Array { get; } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/ManagedBufferBase.cs b/src/SixLabors.Core/Memory/ManagedBufferBase.cs new file mode 100644 index 000000000..1d07b2dac --- /dev/null +++ b/src/SixLabors.Core/Memory/ManagedBufferBase.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; +using System.Runtime.InteropServices; + +namespace SixLabors.Memory +{ + /// + /// Provides a base class for implementations by implementing pinning logic for adaption. + /// + /// The element type + internal abstract class ManagedBufferBase : MemoryManager + where T : struct + { + private GCHandle pinHandle; + + /// + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + if (!this.pinHandle.IsAllocated) + { + this.pinHandle = GCHandle.Alloc(this.GetPinnableObject(), GCHandleType.Pinned); + } + + void* ptr = (void*)this.pinHandle.AddrOfPinnedObject(); + return new MemoryHandle(ptr, this.pinHandle); + } + + /// + public override void Unpin() + { + if (this.pinHandle.IsAllocated) + { + this.pinHandle.Free(); + } + } + + /// + /// Gets the object that should be pinned. + /// + /// The pinnable + protected abstract object GetPinnableObject(); + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/Memory/MemoryAllocator.cs b/src/SixLabors.Core/Memory/MemoryAllocator.cs new file mode 100644 index 000000000..36ce8fcce --- /dev/null +++ b/src/SixLabors.Core/Memory/MemoryAllocator.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.Memory +{ + /// + /// Memory managers are used to allocate memory for image processing operations. + /// + public abstract class MemoryAllocator + { + /// + /// Allocates an , holding a of length . + /// + /// Type of the data stored in the buffer + /// Size of the buffer to allocate + /// The allocation options. + /// A buffer of values of type . + public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + where T : struct; + + /// + /// Allocates an . + /// + /// The requested buffer length + /// The allocation options. + /// The + public abstract IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None); + + /// + /// Releases all retained resources not being in use. + /// Eg: by resetting array pools and letting GC to free the arrays. + /// + public virtual void ReleaseRetainedResources() + { + } + } +} diff --git a/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs b/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs new file mode 100644 index 000000000..9b4c0fda0 --- /dev/null +++ b/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Buffers; + +namespace SixLabors.Memory +{ + /// + /// Implements by newing up arrays by the GC on every allocation requests. + /// + public sealed class SimpleGcMemoryAllocator : MemoryAllocator + { + /// + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + { + return new BasicArrayBuffer(new T[length]); + } + + /// + public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) + { + return new BasicByteBuffer(new byte[length]); + } + } +} \ No newline at end of file diff --git a/src/SixLabors.Core/SixLabors.Core.csproj b/src/SixLabors.Core/SixLabors.Core.csproj index d14738dd5..bc37cd2e2 100644 --- a/src/SixLabors.Core/SixLabors.Core.csproj +++ b/src/SixLabors.Core/SixLabors.Core.csproj @@ -42,6 +42,9 @@ All + + + diff --git a/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryManagerTests.cs b/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryManagerTests.cs new file mode 100644 index 000000000..0068fce91 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryManagerTests.cs @@ -0,0 +1,240 @@ +// 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.Tests; +using Xunit; + +namespace SixLabors.Memory.Tests +{ + public class ArrayPoolMemoryManagerTests + { + 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] + [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] + 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] + public void AllocationOverLargeArrayThreshold_UsesDifferentPool() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + 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] + public void CreateWithAggressivePooling() + { + if (!TestEnvironment.Is64BitProcess) + { + // can lead to OutOfMemoryException + return; + } + + this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithAggressivePooling(); + + Assert.True(this.CheckIsRentingPooledBuffer(4096 * 4096)); + } + + [Fact] + 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 uint dummy; + } + + [StructLayout(LayoutKind.Explicit, Size = MaxPooledBufferSizeInBytes / 5)] + private struct LargeStruct + { + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs b/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs new file mode 100644 index 000000000..fd53db1d8 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.Memory.Tests +{ + internal static class BufferExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetSpan(this IMemoryOwner buffer) + => buffer.Memory.Span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Length(this IMemoryOwner buffer) + => buffer.GetSpan().Length; + + public static ref T GetReference(this IMemoryOwner buffer) + where T : struct => + ref MemoryMarshal.GetReference(buffer.GetSpan()); + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs b/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs new file mode 100644 index 000000000..eca184264 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs @@ -0,0 +1,318 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace SixLabors.Memory.Tests +{ + /// + /// Inherit this class to test an implementation (provided by ). + /// + public abstract class BufferTestSuite + { + protected BufferTestSuite(MemoryAllocator memoryAllocator) + { + this.MemoryAllocator = memoryAllocator; + } + + protected MemoryAllocator MemoryAllocator { get; } + + public struct CustomStruct : IEquatable + { + public long A; + + public byte B; + + public float C; + + public CustomStruct(long a, byte b, float c) + { + this.A = a; + this.B = b; + this.C = c; + } + + public bool Equals(CustomStruct other) + { + return this.A == other.A && this.B == other.B && this.C.Equals(other.C); + } + + public override bool Equals(object obj) + { + return obj is CustomStruct other && this.Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = this.A.GetHashCode(); + hashCode = (hashCode * 397) ^ this.B.GetHashCode(); + hashCode = (hashCode * 397) ^ this.C.GetHashCode(); + return hashCode; + } + } + } + + public static readonly TheoryData LenthValues = new TheoryData { 0, 1, 7, 1023, 1024 }; + + [Theory] + [MemberData(nameof(LenthValues))] + public void HasCorrectLength_byte(int desiredLength) + { + this.TestHasCorrectLength(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void HasCorrectLength_float(int desiredLength) + { + this.TestHasCorrectLength(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void HasCorrectLength_CustomStruct(int desiredLength) + { + this.TestHasCorrectLength(desiredLength); + } + + private void TestHasCorrectLength(int desiredLength) + where T : struct + { + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength)) + { + Assert.Equal(desiredLength, buffer.GetSpan().Length); + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void CanAllocateCleanBuffer_byte(int desiredLength) + { + this.TestCanAllocateCleanBuffer(desiredLength, false); + this.TestCanAllocateCleanBuffer(desiredLength, true); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void CanAllocateCleanBuffer_double(int desiredLength) + { + this.TestCanAllocateCleanBuffer(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void CanAllocateCleanBuffer_CustomStruct(int desiredLength) + { + this.TestCanAllocateCleanBuffer(desiredLength); + } + + private IMemoryOwner Allocate(int desiredLength, AllocationOptions options, bool managedByteBuffer) + where T : struct + { + if (managedByteBuffer) + { + if (!(this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength, options) is IMemoryOwner buffer)) + { + throw new InvalidOperationException("typeof(T) != typeof(byte)"); + } + + return buffer; + } + + return this.MemoryAllocator.Allocate(desiredLength, options); + } + + private void TestCanAllocateCleanBuffer(int desiredLength, bool testManagedByteBuffer = false) + where T : struct, IEquatable + { + ReadOnlySpan expected = new T[desiredLength]; + + for (int i = 0; i < 10; i++) + { + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.Clean, testManagedByteBuffer)) + { + Assert.True(buffer.GetSpan().SequenceEqual(expected)); + } + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void SpanPropertyIsAlwaysTheSame_int(int desiredLength) + { + this.TestSpanPropertyIsAlwaysTheSame(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void SpanPropertyIsAlwaysTheSame_byte(int desiredLength) + { + this.TestSpanPropertyIsAlwaysTheSame(desiredLength, false); + this.TestSpanPropertyIsAlwaysTheSame(desiredLength, true); + } + + private void TestSpanPropertyIsAlwaysTheSame(int desiredLength, bool testManagedByteBuffer = false) + where T : struct + { + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + { + ref T a = ref MemoryMarshal.GetReference(buffer.GetSpan()); + ref T b = ref MemoryMarshal.GetReference(buffer.GetSpan()); + ref T c = ref MemoryMarshal.GetReference(buffer.GetSpan()); + + Assert.True(Unsafe.AreSame(ref a, ref b)); + Assert.True(Unsafe.AreSame(ref b, ref c)); + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void WriteAndReadElements_float(int desiredLength) + { + this.TestWriteAndReadElements(desiredLength, x => x * 1.2f); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void WriteAndReadElements_byte(int desiredLength) + { + this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), false); + this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), true); + } + + private void TestWriteAndReadElements(int desiredLength, Func getExpectedValue, bool testManagedByteBuffer = false) + where T : struct + { + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + { + T[] expectedVals = new T[buffer.Length()]; + + for (int i = 0; i < buffer.Length(); i++) + { + Span span = buffer.GetSpan(); + expectedVals[i] = getExpectedValue(i); + span[i] = expectedVals[i]; + } + + for (int i = 0; i < buffer.Length(); i++) + { + Span span = buffer.GetSpan(); + Assert.Equal(expectedVals[i], span[i]); + } + } + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void IndexingSpan_WhenOutOfRange_Throws_byte(int desiredLength) + { + this.TestIndexOutOfRangeShouldThrow(desiredLength, false); + this.TestIndexOutOfRangeShouldThrow(desiredLength, true); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void IndexingSpan_WhenOutOfRange_Throws_long(int desiredLength) + { + this.TestIndexOutOfRangeShouldThrow(desiredLength); + } + + [Theory] + [MemberData(nameof(LenthValues))] + public void IndexingSpan_WhenOutOfRange_Throws_CustomStruct(int desiredLength) + { + this.TestIndexOutOfRangeShouldThrow(desiredLength); + } + + private T TestIndexOutOfRangeShouldThrow(int desiredLength, bool testManagedByteBuffer = false) + where T : struct, IEquatable + { + var dummy = default(T); + + using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + { + Assert.ThrowsAny( + () => + { + Span span = buffer.GetSpan(); + dummy = span[desiredLength]; + }); + + Assert.ThrowsAny( + () => + { + Span span = buffer.GetSpan(); + dummy = span[desiredLength + 1]; + }); + + Assert.ThrowsAny( + () => + { + Span span = buffer.GetSpan(); + dummy = span[desiredLength + 42]; + }); + } + + return dummy; + } + + [Theory] + [InlineData(1)] + [InlineData(7)] + [InlineData(1024)] + [InlineData(6666)] + public void ManagedByteBuffer_ArrayIsCorrect(int desiredLength) + { + using (IManagedByteBuffer buffer = this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength)) + { + ref byte array0 = ref buffer.Array[0]; + ref byte span0 = ref buffer.GetReference(); + + Assert.True(Unsafe.AreSame(ref span0, ref array0)); + Assert.True(buffer.Array.Length >= buffer.GetSpan().Length); + } + } + + [Fact] + public void GetMemory_ReturnsValidMemory() + { + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(42)) + { + Span span0 = buffer.GetSpan(); + span0[10].A = 30; + Memory memory = buffer.Memory; + + Assert.Equal(42, memory.Length); + Span span1 = memory.Span; + + Assert.Equal(42, span1.Length); + Assert.Equal(30, span1[10].A); + } + } + + [Fact] + public unsafe void GetMemory_ResultIsPinnable() + { + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(42)) + { + Span span0 = buffer.GetSpan(); + span0[10] = 30; + + Memory memory = buffer.Memory; + + using (MemoryHandle h = memory.Pin()) + { + int* ptr = (int*)h.Pointer; + Assert.Equal(30, ptr[10]); + } + } + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryManagerTests.cs b/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryManagerTests.cs new file mode 100644 index 000000000..a6ddeb050 --- /dev/null +++ b/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryManagerTests.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.Memory.Tests +{ + public class SimpleGcMemoryManagerTests + { + public class BufferTests : BufferTestSuite + { + public BufferTests() + : base(new SimpleGcMemoryAllocator()) + { + } + } + } +} \ No newline at end of file diff --git a/tests/SixLabors.Core.Tests/SixLabors.Core.Tests.csproj b/tests/SixLabors.Core.Tests/SixLabors.Core.Tests.csproj index 6869491e3..d23fb956a 100644 --- a/tests/SixLabors.Core.Tests/SixLabors.Core.Tests.csproj +++ b/tests/SixLabors.Core.Tests/SixLabors.Core.Tests.csproj @@ -14,6 +14,7 @@ false full SixLabors.Tests + true diff --git a/tests/SixLabors.Core.Tests/TestEnvironment.cs b/tests/SixLabors.Core.Tests/TestEnvironment.cs new file mode 100644 index 000000000..77be7bfe1 --- /dev/null +++ b/tests/SixLabors.Core.Tests/TestEnvironment.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.Tests +{ + internal class TestEnvironment + { + internal static bool Is64BitProcess => IntPtr.Size == 8; + } +} \ No newline at end of file