Merge pull request #14 from SixLabors/af/public-memory-api

Publish SixLabors.Memory
This commit is contained in:
James Jackson-South 2018-07-27 15:58:28 +01:00 коммит произвёл GitHub
Родитель eee920dcfb 6dd6c949f0
Коммит fa868db8a4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 1155 добавлений и 6 удалений

Просмотреть файл

@ -2,4 +2,5 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1200/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1200/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1201/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1201/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1401/@EntryIndexedValue">DO_NOT_SHOW</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1401/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1600/@EntryIndexedValue">DO_NOT_SHOW</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StyleCop_002ESA1600/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String></wpf:ResourceDictionary>

Просмотреть файл

@ -19,8 +19,10 @@ namespace SixLabors
/// <param name="target">The target object, which cannot be null.</param> /// <param name="target">The target object, which cannot be null.</param>
/// <param name="parameterName">The name of the parameter that is to be checked.</param> /// <param name="parameterName">The name of the parameter that is to be checked.</param>
/// <exception cref="ArgumentNullException"><paramref name="target"/> is null</exception> /// <exception cref="ArgumentNullException"><paramref name="target"/> is null</exception>
/// <typeparam name="T">The type of the object to verify</typeparam>
[Conditional("DEBUG")] [Conditional("DEBUG")]
public static void NotNull(object target, string parameterName) public static void NotNull<T>(T target, string parameterName)
where T : class
{ {
if (target == null) if (target == null)
{ {

Просмотреть файл

@ -22,7 +22,9 @@ namespace SixLabors
/// <param name="parameterName">The name of the parameter that is to be checked.</param> /// <param name="parameterName">The name of the parameter that is to be checked.</param>
/// <param name="message">The error message, if any to add to the exception.</param> /// <param name="message">The error message, if any to add to the exception.</param>
/// <exception cref="ArgumentNullException"><paramref name="target"/> is null</exception> /// <exception cref="ArgumentNullException"><paramref name="target"/> is null</exception>
public static void NotNull(object target, string parameterName, string message = "") /// <typeparam name="T">The type of the object to verify</typeparam>
public static void NotNull<T>(T target, string parameterName, string message = "")
where T : class
{ {
if (target == null) if (target == null)
{ {

Просмотреть файл

@ -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,85 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.Memory.Internals;
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,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,61 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
namespace SixLabors.Memory.Internals
{
/// <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; }
/// <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.Internals
{
/// <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,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.Internals
{
/// <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,26 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Buffers;
using SixLabors.Memory.Internals;
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]);
}
}
}

Просмотреть файл

@ -42,6 +42,9 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004"> <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004">
<PrivateAssets>All</PrivateAssets> <PrivateAssets>All</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.Buffers" Version="4.5.0" />
<PackageReference Include="System.Memory" Version="4.5.1" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.5.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.1'"> <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.1'">
<PackageReference Include="System.Numerics.Vectors" Version="4.4.0" /> <PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />

Просмотреть файл

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

Просмотреть файл

@ -39,7 +39,7 @@ namespace SixLabors.Helpers.Tests
{ {
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {
DebugGuard.NotNull(null, "myParamName"); DebugGuard.NotNull((object)null, "myParamName");
}); });
} }

Просмотреть файл

@ -21,7 +21,7 @@ namespace SixLabors.Helpers.Tests
{ {
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {
Guard.NotNull(null, "myParamName"); Guard.NotNull((object)null, "myParamName");
}); });
} }
@ -30,7 +30,7 @@ namespace SixLabors.Helpers.Tests
{ {
var exception = Assert.Throws<ArgumentNullException>(() => var exception = Assert.Throws<ArgumentNullException>(() =>
{ {
Guard.NotNull(null, "myParamName", "myTestMessage"); Guard.NotNull((object)null, "myParamName", "myTestMessage");
}); });
Assert.Equal("myParamName", exception.ParamName); Assert.Equal("myParamName", exception.ParamName);

Просмотреть файл

@ -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())
{
}
}
}
}

Просмотреть файл

@ -14,6 +14,7 @@
<GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute> <GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute>
<DebugType Condition="$(codecov) != ''">full</DebugType> <DebugType Condition="$(codecov) != ''">full</DebugType>
<RootNamespace>SixLabors.Tests</RootNamespace> <RootNamespace>SixLabors.Tests</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

Просмотреть файл

@ -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;
}
}