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