mirror of https://github.com/SixLabors/ImageSharp
18 changed files with 1159 additions and 1 deletions
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Options for allocating buffers.
|
|||
/// </summary>
|
|||
public enum AllocationOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Indicates that the buffer should just be allocated.
|
|||
/// </summary>
|
|||
None, |
|||
|
|||
/// <summary>
|
|||
/// Indicates that the allocated buffer should be cleaned following allocation.
|
|||
/// </summary>
|
|||
Clean |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Contains <see cref="Buffer{T}"/> and <see cref="ManagedByteBuffer"/>
|
|||
/// </summary>
|
|||
public partial class ArrayPoolMemoryAllocator |
|||
{ |
|||
/// <summary>
|
|||
/// The buffer implementation of <see cref="ArrayPoolMemoryAllocator"/>.
|
|||
/// </summary>
|
|||
private class Buffer<T> : ManagedBufferBase<T> |
|||
where T : struct |
|||
{ |
|||
/// <summary>
|
|||
/// The length of the buffer
|
|||
/// </summary>
|
|||
private readonly int length; |
|||
|
|||
/// <summary>
|
|||
/// A weak reference to the source pool.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 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 <see cref="ArrayPoolMemoryAllocator.ReleaseRetainedResources"/>, regardless of having buffer instances still being in use.
|
|||
/// </remarks>
|
|||
private WeakReference<ArrayPool<byte>> sourcePoolReference; |
|||
|
|||
public Buffer(byte[] data, int length, ArrayPool<byte> sourcePool) |
|||
{ |
|||
this.Data = data; |
|||
this.length = length; |
|||
this.sourcePoolReference = new WeakReference<ArrayPool<byte>>(sourcePool); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the buffer as a byte array.
|
|||
/// </summary>
|
|||
protected byte[] Data { get; private set; } |
|||
|
|||
/// <inheritdoc />
|
|||
public override Span<T> GetSpan() => MemoryMarshal.Cast<byte, T>(this.Data.AsSpan()).Slice(0, this.length); |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (!disposing || this.Data == null || this.sourcePoolReference == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (this.sourcePoolReference.TryGetTarget(out ArrayPool<byte> pool)) |
|||
{ |
|||
pool.Return(this.Data); |
|||
} |
|||
|
|||
this.sourcePoolReference = null; |
|||
this.Data = null; |
|||
} |
|||
|
|||
protected override object GetPinnableObject() => this.Data; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The <see cref="IManagedByteBuffer"/> implementation of <see cref="ArrayPoolMemoryAllocator"/>.
|
|||
/// </summary>
|
|||
private class ManagedByteBuffer : Buffer<byte>, IManagedByteBuffer |
|||
{ |
|||
public ManagedByteBuffer(byte[] data, int length, ArrayPool<byte> sourcePool) |
|||
: base(data, length, sourcePool) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public byte[] Array => this.Data; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Contains common factory methods and configuration constants.
|
|||
/// </summary>
|
|||
public partial class ArrayPoolMemoryAllocator |
|||
{ |
|||
/// <summary>
|
|||
/// 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.
|
|||
/// </summary>
|
|||
internal const int DefaultMaxPooledBufferSizeInBytes = 24 * 1024 * 1024; |
|||
|
|||
/// <summary>
|
|||
/// The value for: The threshold to pool arrays in <see cref="largeArrayPool"/> which has less buckets for memory safety.
|
|||
/// </summary>
|
|||
private const int DefaultBufferSelectorThresholdInBytes = 8 * 1024 * 1024; |
|||
|
|||
/// <summary>
|
|||
/// The default bucket count for <see cref="largeArrayPool"/>.
|
|||
/// </summary>
|
|||
private const int DefaultLargePoolBucketCount = 6; |
|||
|
|||
/// <summary>
|
|||
/// The default bucket count for <see cref="normalArrayPool"/>.
|
|||
/// </summary>
|
|||
private const int DefaultNormalPoolBucketCount = 16; |
|||
|
|||
/// <summary>
|
|||
/// This is the default. Should be good for most use cases.
|
|||
/// </summary>
|
|||
/// <returns>The memory manager</returns>
|
|||
public static ArrayPoolMemoryAllocator CreateDefault() |
|||
{ |
|||
return new ArrayPoolMemoryAllocator( |
|||
DefaultMaxPooledBufferSizeInBytes, |
|||
DefaultBufferSelectorThresholdInBytes, |
|||
DefaultLargePoolBucketCount, |
|||
DefaultNormalPoolBucketCount); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// For environments with limited memory capabilities. Only small images are pooled, which can result in reduced througput.
|
|||
/// </summary>
|
|||
/// <returns>The memory manager</returns>
|
|||
public static ArrayPoolMemoryAllocator CreateWithModeratePooling() |
|||
{ |
|||
return new ArrayPoolMemoryAllocator(1024 * 1024, 32 * 1024, 16, 24); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Only pool small buffers like image rows.
|
|||
/// </summary>
|
|||
/// <returns>The memory manager</returns>
|
|||
public static ArrayPoolMemoryAllocator CreateWithMinimalPooling() |
|||
{ |
|||
return new ArrayPoolMemoryAllocator(64 * 1024, 32 * 1024, 8, 24); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// RAM is not an issue for me, gimme maximum througput!
|
|||
/// </summary>
|
|||
/// <returns>The memory manager</returns>
|
|||
public static ArrayPoolMemoryAllocator CreateWithAggressivePooling() |
|||
{ |
|||
return new ArrayPoolMemoryAllocator(128 * 1024 * 1024, 32 * 1024 * 1024, 16, 32); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Implements <see cref="MemoryAllocator"/> by allocating memory from <see cref="ArrayPool{T}"/>.
|
|||
/// </summary>
|
|||
public sealed partial class ArrayPoolMemoryAllocator : MemoryAllocator |
|||
{ |
|||
private readonly int maxArraysPerBucketNormalPool; |
|||
|
|||
private readonly int maxArraysPerBucketLargePool; |
|||
|
|||
/// <summary>
|
|||
/// The <see cref="ArrayPool{T}"/> for small-to-medium buffers which is not kept clean.
|
|||
/// </summary>
|
|||
private ArrayPool<byte> normalArrayPool; |
|||
|
|||
/// <summary>
|
|||
/// The <see cref="ArrayPool{T}"/> for huge buffers, which is not kept clean.
|
|||
/// </summary>
|
|||
private ArrayPool<byte> largeArrayPool; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ArrayPoolMemoryAllocator"/> class.
|
|||
/// </summary>
|
|||
public ArrayPoolMemoryAllocator() |
|||
: this(DefaultMaxPooledBufferSizeInBytes, DefaultBufferSelectorThresholdInBytes) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ArrayPoolMemoryAllocator"/> class.
|
|||
/// </summary>
|
|||
/// <param name="maxPoolSizeInBytes">The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated.</param>
|
|||
public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes) |
|||
: this(maxPoolSizeInBytes, GetLargeBufferThresholdInBytes(maxPoolSizeInBytes)) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ArrayPoolMemoryAllocator"/> class.
|
|||
/// </summary>
|
|||
/// <param name="maxPoolSizeInBytes">The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated.</param>
|
|||
/// <param name="poolSelectorThresholdInBytes">Arrays over this threshold will be pooled in <see cref="largeArrayPool"/> which has less buckets for memory safety.</param>
|
|||
public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes) |
|||
: this(maxPoolSizeInBytes, poolSelectorThresholdInBytes, DefaultLargePoolBucketCount, DefaultNormalPoolBucketCount) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ArrayPoolMemoryAllocator"/> class.
|
|||
/// </summary>
|
|||
/// <param name="maxPoolSizeInBytes">The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated.</param>
|
|||
/// <param name="poolSelectorThresholdInBytes">The threshold to pool arrays in <see cref="largeArrayPool"/> which has less buckets for memory safety.</param>
|
|||
/// <param name="maxArraysPerBucketLargePool">Max arrays per bucket for the large array pool</param>
|
|||
/// <param name="maxArraysPerBucketNormalPool">Max arrays per bucket for the normal array pool</param>
|
|||
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(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the maximum size of pooled arrays in bytes.
|
|||
/// </summary>
|
|||
public int MaxPoolSizeInBytes { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the threshold to pool arrays in <see cref="largeArrayPool"/> which has less buckets for memory safety.
|
|||
/// </summary>
|
|||
public int PoolSelectorThresholdInBytes { get; } |
|||
|
|||
/// <inheritdoc />
|
|||
public override void ReleaseRetainedResources() |
|||
{ |
|||
this.InitArrayPools(); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) |
|||
{ |
|||
int itemSizeBytes = Unsafe.SizeOf<T>(); |
|||
int bufferSizeInBytes = length * itemSizeBytes; |
|||
|
|||
ArrayPool<byte> pool = this.GetArrayPool(bufferSizeInBytes); |
|||
byte[] byteArray = pool.Rent(bufferSizeInBytes); |
|||
|
|||
var buffer = new Buffer<T>(byteArray, length, pool); |
|||
if (options == AllocationOptions.Clean) |
|||
{ |
|||
buffer.GetSpan().Clear(); |
|||
} |
|||
|
|||
return buffer; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) |
|||
{ |
|||
ArrayPool<byte> 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<byte> GetArrayPool(int bufferSizeInBytes) |
|||
{ |
|||
return bufferSizeInBytes <= this.PoolSelectorThresholdInBytes ? this.normalArrayPool : this.largeArrayPool; |
|||
} |
|||
|
|||
private void InitArrayPools() |
|||
{ |
|||
this.largeArrayPool = ArrayPool<byte>.Create(this.MaxPoolSizeInBytes, this.maxArraysPerBucketLargePool); |
|||
this.normalArrayPool = ArrayPool<byte>.Create(this.PoolSelectorThresholdInBytes, this.maxArraysPerBucketNormalPool); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Wraps an array as an <see cref="IManagedByteBuffer"/> instance.
|
|||
/// </summary>
|
|||
/// <inheritdoc />
|
|||
internal class BasicArrayBuffer<T> : ManagedBufferBase<T> |
|||
where T : struct |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BasicArrayBuffer{T}"/> class
|
|||
/// </summary>
|
|||
/// <param name="array">The array</param>
|
|||
/// <param name="length">The length of the buffer</param>
|
|||
public BasicArrayBuffer(T[] array, int length) |
|||
{ |
|||
DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length)); |
|||
this.Array = array; |
|||
this.Length = length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BasicArrayBuffer{T}"/> class
|
|||
/// </summary>
|
|||
/// <param name="array">The array</param>
|
|||
public BasicArrayBuffer(T[] array) |
|||
: this(array, array.Length) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the array
|
|||
/// </summary>
|
|||
public T[] Array { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the length
|
|||
/// </summary>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Returns a reference to specified element of the buffer.
|
|||
/// </summary>
|
|||
/// <param name="index">The index</param>
|
|||
/// <returns>The reference to the specified element</returns>
|
|||
public ref T this[int index] |
|||
{ |
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
get |
|||
{ |
|||
DebugGuard.MustBeLessThan(index, this.Length, nameof(index)); |
|||
|
|||
Span<T> span = this.GetSpan(); |
|||
return ref span[index]; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override Span<T> GetSpan() => this.Array.AsSpan(0, this.Length); |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override object GetPinnableObject() |
|||
{ |
|||
return this.Array; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Provides an <see cref="IManagedByteBuffer"/> based on <see cref="BasicArrayBuffer{T}"/>.
|
|||
/// </summary>
|
|||
internal sealed class BasicByteBuffer : BasicArrayBuffer<byte>, IManagedByteBuffer |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BasicByteBuffer"/> class
|
|||
/// </summary>
|
|||
/// <param name="array">The byte array</param>
|
|||
internal BasicByteBuffer(byte[] array) |
|||
: base(array) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Buffers; |
|||
|
|||
namespace SixLabors.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a byte buffer backed by a managed array. Useful for interop with classic .NET API-s.
|
|||
/// </summary>
|
|||
public interface IManagedByteBuffer : IMemoryOwner<byte> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the managed array backing this buffer instance.
|
|||
/// </summary>
|
|||
byte[] Array { get; } |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Provides a base class for <see cref="IMemoryOwner{T}"/> implementations by implementing pinning logic for <see cref="MemoryManager{T}"/> adaption.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The element type</typeparam>
|
|||
internal abstract class ManagedBufferBase<T> : MemoryManager<T> |
|||
where T : struct |
|||
{ |
|||
private GCHandle pinHandle; |
|||
|
|||
/// <inheritdoc />
|
|||
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); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override void Unpin() |
|||
{ |
|||
if (this.pinHandle.IsAllocated) |
|||
{ |
|||
this.pinHandle.Free(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the object that should be pinned.
|
|||
/// </summary>
|
|||
/// <returns>The pinnable <see cref="object"/></returns>
|
|||
protected abstract object GetPinnableObject(); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Buffers; |
|||
|
|||
namespace SixLabors.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Memory managers are used to allocate memory for image processing operations.
|
|||
/// </summary>
|
|||
public abstract class MemoryAllocator |
|||
{ |
|||
/// <summary>
|
|||
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="System.Memory{T}"/> of length <paramref name="length"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">Type of the data stored in the buffer</typeparam>
|
|||
/// <param name="length">Size of the buffer to allocate</param>
|
|||
/// <param name="options">The allocation options.</param>
|
|||
/// <returns>A buffer of values of type <typeparamref name="T"/>.</returns>
|
|||
public abstract IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) |
|||
where T : struct; |
|||
|
|||
/// <summary>
|
|||
/// Allocates an <see cref="IManagedByteBuffer"/>.
|
|||
/// </summary>
|
|||
/// <param name="length">The requested buffer length</param>
|
|||
/// <param name="options">The allocation options.</param>
|
|||
/// <returns>The <see cref="IManagedByteBuffer"/></returns>
|
|||
public abstract IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None); |
|||
|
|||
/// <summary>
|
|||
/// Releases all retained resources not being in use.
|
|||
/// Eg: by resetting array pools and letting GC to free the arrays.
|
|||
/// </summary>
|
|||
public virtual void ReleaseRetainedResources() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Buffers; |
|||
|
|||
namespace SixLabors.Memory |
|||
{ |
|||
/// <summary>
|
|||
/// Implements <see cref="MemoryAllocator"/> by newing up arrays by the GC on every allocation requests.
|
|||
/// </summary>
|
|||
public sealed class SimpleGcMemoryAllocator : MemoryAllocator |
|||
{ |
|||
/// <inheritdoc />
|
|||
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) |
|||
{ |
|||
return new BasicArrayBuffer<T>(new T[length]); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) |
|||
{ |
|||
return new BasicByteBuffer(new byte[length]); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
|
|||
/// <summary>
|
|||
/// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location
|
|||
/// </summary>
|
|||
private bool CheckIsRentingPooledBuffer<T>(int length) |
|||
where T : struct |
|||
{ |
|||
IMemoryOwner<T> buffer = this.MemoryAllocator.Allocate<T>(length); |
|||
ref T ptrToPrevPosition0 = ref buffer.GetReference(); |
|||
buffer.Dispose(); |
|||
|
|||
buffer = this.MemoryAllocator.Allocate<T>(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<Exception>(() => { new ArrayPoolMemoryAllocator(100, 200); }); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(32)] |
|||
[InlineData(512)] |
|||
[InlineData(MaxPooledBufferSizeInBytes - 1)] |
|||
public void SmallBuffersArePooled_OfByte(int size) |
|||
{ |
|||
Assert.True(this.CheckIsRentingPooledBuffer<byte>(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<byte>(size)); |
|||
} |
|||
|
|||
[Fact] |
|||
public unsafe void SmallBuffersArePooled_OfBigValueType() |
|||
{ |
|||
int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) - 1; |
|||
|
|||
Assert.True(this.CheckIsRentingPooledBuffer<LargeStruct>(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<LargeStruct>(count)); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(AllocationOptions.None)] |
|||
[InlineData(AllocationOptions.Clean)] |
|||
public void CleaningRequests_AreControlledByAllocationParameter_Clean(AllocationOptions options) |
|||
{ |
|||
using (IMemoryOwner<int> firstAlloc = this.MemoryAllocator.Allocate<int>(42)) |
|||
{ |
|||
firstAlloc.GetSpan().Fill(666); |
|||
} |
|||
|
|||
using (IMemoryOwner<int> secondAlloc = this.MemoryAllocator.Allocate<int>(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<int> buffer = this.MemoryAllocator.Allocate<int>(32); |
|||
ref int ptrToPrev0 = ref MemoryMarshal.GetReference(buffer.GetSpan()); |
|||
|
|||
if (!keepBufferAlive) |
|||
{ |
|||
buffer.Dispose(); |
|||
} |
|||
|
|||
this.MemoryAllocator.ReleaseRetainedResources(); |
|||
|
|||
buffer = this.MemoryAllocator.Allocate<int>(32); |
|||
|
|||
Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.GetReference())); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed() |
|||
{ |
|||
IMemoryOwner<int> buffer = this.MemoryAllocator.Allocate<int>(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<int> small = this.MemoryAllocator.Allocate<int>(arrayLengthThreshold - 1); |
|||
ref int ptr2Small = ref small.GetReference(); |
|||
small.Dispose(); |
|||
|
|||
IMemoryOwner<int> large = this.MemoryAllocator.Allocate<int>(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<Rgba32>(4096 * 4096)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CreateDefault() |
|||
{ |
|||
if (!TestEnvironment.Is64BitProcess) |
|||
{ |
|||
// can lead to OutOfMemoryException
|
|||
return; |
|||
} |
|||
|
|||
this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault(); |
|||
|
|||
Assert.False(this.CheckIsRentingPooledBuffer<Rgba32>(2 * 4096 * 4096)); |
|||
Assert.True(this.CheckIsRentingPooledBuffer<Rgba32>(2048 * 2048)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CreateWithModeratePooling() |
|||
{ |
|||
if (!TestEnvironment.Is64BitProcess) |
|||
{ |
|||
// can lead to OutOfMemoryException
|
|||
return; |
|||
} |
|||
|
|||
this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling(); |
|||
|
|||
Assert.False(this.CheckIsRentingPooledBuffer<Rgba32>(2048 * 2048)); |
|||
Assert.True(this.CheckIsRentingPooledBuffer<Rgba32>(1024 * 16)); |
|||
} |
|||
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
private struct Rgba32 |
|||
{ |
|||
private uint dummy; |
|||
} |
|||
|
|||
[StructLayout(LayoutKind.Explicit, Size = MaxPooledBufferSizeInBytes / 5)] |
|||
private struct LargeStruct |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -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<T> GetSpan<T>(this IMemoryOwner<T> buffer) |
|||
=> buffer.Memory.Span; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static int Length<T>(this IMemoryOwner<T> buffer) |
|||
=> buffer.GetSpan().Length; |
|||
|
|||
public static ref T GetReference<T>(this IMemoryOwner<T> buffer) |
|||
where T : struct => |
|||
ref MemoryMarshal.GetReference(buffer.GetSpan()); |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Inherit this class to test an <see cref="IMemoryOwner{T}"/> implementation (provided by <see cref="MemoryAllocator"/>).
|
|||
/// </summary>
|
|||
public abstract class BufferTestSuite |
|||
{ |
|||
protected BufferTestSuite(MemoryAllocator memoryAllocator) |
|||
{ |
|||
this.MemoryAllocator = memoryAllocator; |
|||
} |
|||
|
|||
protected MemoryAllocator MemoryAllocator { get; } |
|||
|
|||
public struct CustomStruct : IEquatable<CustomStruct> |
|||
{ |
|||
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<int> LenthValues = new TheoryData<int> { 0, 1, 7, 1023, 1024 }; |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void HasCorrectLength_byte(int desiredLength) |
|||
{ |
|||
this.TestHasCorrectLength<byte>(desiredLength); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void HasCorrectLength_float(int desiredLength) |
|||
{ |
|||
this.TestHasCorrectLength<float>(desiredLength); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void HasCorrectLength_CustomStruct(int desiredLength) |
|||
{ |
|||
this.TestHasCorrectLength<CustomStruct>(desiredLength); |
|||
} |
|||
|
|||
private void TestHasCorrectLength<T>(int desiredLength) |
|||
where T : struct |
|||
{ |
|||
using (IMemoryOwner<T> buffer = this.MemoryAllocator.Allocate<T>(desiredLength)) |
|||
{ |
|||
Assert.Equal(desiredLength, buffer.GetSpan().Length); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void CanAllocateCleanBuffer_byte(int desiredLength) |
|||
{ |
|||
this.TestCanAllocateCleanBuffer<byte>(desiredLength, false); |
|||
this.TestCanAllocateCleanBuffer<byte>(desiredLength, true); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void CanAllocateCleanBuffer_double(int desiredLength) |
|||
{ |
|||
this.TestCanAllocateCleanBuffer<double>(desiredLength); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void CanAllocateCleanBuffer_CustomStruct(int desiredLength) |
|||
{ |
|||
this.TestCanAllocateCleanBuffer<CustomStruct>(desiredLength); |
|||
} |
|||
|
|||
private IMemoryOwner<T> Allocate<T>(int desiredLength, AllocationOptions options, bool managedByteBuffer) |
|||
where T : struct |
|||
{ |
|||
if (managedByteBuffer) |
|||
{ |
|||
if (!(this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength, options) is IMemoryOwner<T> buffer)) |
|||
{ |
|||
throw new InvalidOperationException("typeof(T) != typeof(byte)"); |
|||
} |
|||
|
|||
return buffer; |
|||
} |
|||
|
|||
return this.MemoryAllocator.Allocate<T>(desiredLength, options); |
|||
} |
|||
|
|||
private void TestCanAllocateCleanBuffer<T>(int desiredLength, bool testManagedByteBuffer = false) |
|||
where T : struct, IEquatable<T> |
|||
{ |
|||
ReadOnlySpan<T> expected = new T[desiredLength]; |
|||
|
|||
for (int i = 0; i < 10; i++) |
|||
{ |
|||
using (IMemoryOwner<T> buffer = this.Allocate<T>(desiredLength, AllocationOptions.Clean, testManagedByteBuffer)) |
|||
{ |
|||
Assert.True(buffer.GetSpan().SequenceEqual(expected)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void SpanPropertyIsAlwaysTheSame_int(int desiredLength) |
|||
{ |
|||
this.TestSpanPropertyIsAlwaysTheSame<int>(desiredLength); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void SpanPropertyIsAlwaysTheSame_byte(int desiredLength) |
|||
{ |
|||
this.TestSpanPropertyIsAlwaysTheSame<byte>(desiredLength, false); |
|||
this.TestSpanPropertyIsAlwaysTheSame<byte>(desiredLength, true); |
|||
} |
|||
|
|||
private void TestSpanPropertyIsAlwaysTheSame<T>(int desiredLength, bool testManagedByteBuffer = false) |
|||
where T : struct |
|||
{ |
|||
using (IMemoryOwner<T> buffer = this.Allocate<T>(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<float>(desiredLength, x => x * 1.2f); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void WriteAndReadElements_byte(int desiredLength) |
|||
{ |
|||
this.TestWriteAndReadElements<byte>(desiredLength, x => (byte)(x + 1), false); |
|||
this.TestWriteAndReadElements<byte>(desiredLength, x => (byte)(x + 1), true); |
|||
} |
|||
|
|||
private void TestWriteAndReadElements<T>(int desiredLength, Func<int, T> getExpectedValue, bool testManagedByteBuffer = false) |
|||
where T : struct |
|||
{ |
|||
using (IMemoryOwner<T> buffer = this.Allocate<T>(desiredLength, AllocationOptions.None, testManagedByteBuffer)) |
|||
{ |
|||
T[] expectedVals = new T[buffer.Length()]; |
|||
|
|||
for (int i = 0; i < buffer.Length(); i++) |
|||
{ |
|||
Span<T> span = buffer.GetSpan(); |
|||
expectedVals[i] = getExpectedValue(i); |
|||
span[i] = expectedVals[i]; |
|||
} |
|||
|
|||
for (int i = 0; i < buffer.Length(); i++) |
|||
{ |
|||
Span<T> span = buffer.GetSpan(); |
|||
Assert.Equal(expectedVals[i], span[i]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void IndexingSpan_WhenOutOfRange_Throws_byte(int desiredLength) |
|||
{ |
|||
this.TestIndexOutOfRangeShouldThrow<byte>(desiredLength, false); |
|||
this.TestIndexOutOfRangeShouldThrow<byte>(desiredLength, true); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void IndexingSpan_WhenOutOfRange_Throws_long(int desiredLength) |
|||
{ |
|||
this.TestIndexOutOfRangeShouldThrow<long>(desiredLength); |
|||
} |
|||
|
|||
[Theory] |
|||
[MemberData(nameof(LenthValues))] |
|||
public void IndexingSpan_WhenOutOfRange_Throws_CustomStruct(int desiredLength) |
|||
{ |
|||
this.TestIndexOutOfRangeShouldThrow<CustomStruct>(desiredLength); |
|||
} |
|||
|
|||
private T TestIndexOutOfRangeShouldThrow<T>(int desiredLength, bool testManagedByteBuffer = false) |
|||
where T : struct, IEquatable<T> |
|||
{ |
|||
var dummy = default(T); |
|||
|
|||
using (IMemoryOwner<T> buffer = this.Allocate<T>(desiredLength, AllocationOptions.None, testManagedByteBuffer)) |
|||
{ |
|||
Assert.ThrowsAny<Exception>( |
|||
() => |
|||
{ |
|||
Span<T> span = buffer.GetSpan(); |
|||
dummy = span[desiredLength]; |
|||
}); |
|||
|
|||
Assert.ThrowsAny<Exception>( |
|||
() => |
|||
{ |
|||
Span<T> span = buffer.GetSpan(); |
|||
dummy = span[desiredLength + 1]; |
|||
}); |
|||
|
|||
Assert.ThrowsAny<Exception>( |
|||
() => |
|||
{ |
|||
Span<T> 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<CustomStruct> buffer = this.MemoryAllocator.Allocate<CustomStruct>(42)) |
|||
{ |
|||
Span<CustomStruct> span0 = buffer.GetSpan(); |
|||
span0[10].A = 30; |
|||
Memory<CustomStruct> memory = buffer.Memory; |
|||
|
|||
Assert.Equal(42, memory.Length); |
|||
Span<CustomStruct> span1 = memory.Span; |
|||
|
|||
Assert.Equal(42, span1.Length); |
|||
Assert.Equal(30, span1[10].A); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public unsafe void GetMemory_ResultIsPinnable() |
|||
{ |
|||
using (IMemoryOwner<int> buffer = this.MemoryAllocator.Allocate<int>(42)) |
|||
{ |
|||
Span<int> span0 = buffer.GetSpan(); |
|||
span0[10] = 30; |
|||
|
|||
Memory<int> memory = buffer.Memory; |
|||
|
|||
using (MemoryHandle h = memory.Pin()) |
|||
{ |
|||
int* ptr = (int*)h.Pointer; |
|||
Assert.Equal(30, ptr[10]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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()) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue