Browse Source

UniformUnmanagedMemoryPoolMemoryAllocator

af/UniformUnmanagedMemoryPoolMemoryAllocator-02-MemoryGuards
Anton Firszov 5 years ago
parent
commit
7c1a1afc61
  1. 3
      Directory.Build.props
  2. 2
      src/ImageSharp/Advanced/AotCompilerTools.cs
  3. 2
      src/ImageSharp/Configuration.cs
  4. 2
      src/ImageSharp/ImageSharp.csproj
  5. 13
      src/ImageSharp/Memory/Allocators/AllocationOptions.cs
  6. 10
      src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
  7. 16
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs
  8. 2
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs
  9. 25
      src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs
  10. 18
      src/ImageSharp/Memory/Allocators/IManagedByteBuffer.cs
  11. 5
      src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
  12. 20
      src/ImageSharp/Memory/Allocators/Internals/BasicByteBuffer.cs
  13. 115
      src/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs
  14. 5
      src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
  15. 40
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  16. 89
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.Buffer{T}.cs
  17. 323
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs
  18. 65
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
  19. 80
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
  20. 40
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  21. 57
      src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
  22. 8
      src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
  23. 149
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  24. 32
      src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
  25. 86
      src/ImageSharp/Memory/Buffer2D{T}.cs
  26. 7
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
  27. 74
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  28. 110
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  29. 17
      src/ImageSharp/Memory/MemoryAllocatorExtensions.cs
  30. 2
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  31. 4
      tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj
  32. 25
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs
  33. 2
      tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs
  34. 1
      tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj
  35. 241
      tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs
  36. 5
      tests/ImageSharp.Tests.ProfilingSandbox/Program.cs
  37. 2
      tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
  38. 4
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  39. 2
      tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs
  40. 2
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs
  41. 2
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  42. 6
      tests/ImageSharp.Tests/Image/ImageFrameTests.cs
  43. 6
      tests/ImageSharp.Tests/Image/ImageTests.cs
  44. 3
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  45. 11
      tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs
  46. 61
      tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs
  47. 10
      tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
  48. 101
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs
  49. 292
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs
  50. 289
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  51. 184
      tests/ImageSharp.Tests/Memory/Allocators/UnmanagedMemoryHandleTests.cs
  52. 2
      tests/ImageSharp.Tests/Memory/Buffer2DTests.cs
  53. 121
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
  54. 12
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs
  55. 2
      tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs
  56. 2
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  57. 10
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  58. 12
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs
  59. 29
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  60. 8
      tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs
  61. 3
      tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

3
Directory.Build.props

@ -26,4 +26,7 @@
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup>
<DefineConstants Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)','netcoreapp3.1'))">$(DefineConstants);NETCORE31COMPATIBLE</DefineConstants>
</PropertyGroup>
</Project>

2
src/ImageSharp/Advanced/AotCompilerTools.cs

@ -524,7 +524,7 @@ namespace SixLabors.ImageSharp.Advanced
private static void AotCompileMemoryManagers<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
AotCompileMemoryManager<TPixel, ArrayPoolMemoryAllocator>();
AotCompileMemoryManager<TPixel, UniformUnmanagedMemoryPoolMemoryAllocator>();
AotCompileMemoryManager<TPixel, SimpleGcMemoryAllocator>();
}

2
src/ImageSharp/Configuration.cs

@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Gets or sets the <see cref="MemoryAllocator"/> that is currently in use.
/// </summary>
public MemoryAllocator MemoryAllocator { get; set; } = ArrayPoolMemoryAllocator.CreateDefault();
public MemoryAllocator MemoryAllocator { get; set; } = MemoryAllocator.CreateDefault();
/// <summary>
/// Gets the maximum header size of all the formats.

2
src/ImageSharp/ImageSharp.csproj

@ -13,8 +13,8 @@
<PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Png Tga NetCore</PackageTags>
<Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
<DefineConstants Condition="'$(Configuration)' == 'Debug-InnerLoop'">$(DefineConstants);DEBUG</DefineConstants>
</PropertyGroup>
<Choose>
<When Condition="$(SIXLABORS_TESTING) == true">
<PropertyGroup>

13
src/ImageSharp/Memory/Allocators/AllocationOptions.cs

@ -1,21 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Options for allocating buffers.
/// </summary>
[Flags]
public enum AllocationOptions
{
/// <summary>
/// Indicates that the buffer should just be allocated.
/// </summary>
None,
None = 0,
/// <summary>
/// Indicates that the allocated buffer should be cleaned following allocation.
/// </summary>
Clean
Clean = 1,
/// <summary>
/// Affects only group allocations.
/// Indicates that the requested <see cref="MemoryGroup{T}"/> or <see cref="Buffer2D{T}"/> should be made of contiguous blocks up to <see cref="int.MaxValue"/>.
/// </summary>
Contiguous = 2
}
}

10
src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs

@ -0,0 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory
{
internal static class AllocationOptionsExtensions
{
public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag;
}
}

16
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs

@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Contains <see cref="Buffer{T}"/> and <see cref="ManagedByteBuffer"/>.
/// Contains <see cref="Buffer{T}"/>.
/// </summary>
public partial class ArrayPoolMemoryAllocator
{
@ -87,19 +87,5 @@ namespace SixLabors.ImageSharp.Memory
throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer<T>");
}
}
/// <summary>
/// The <see cref="IManagedByteBuffer"/> implementation of <see cref="ArrayPoolMemoryAllocator"/>.
/// </summary>
private sealed class ManagedByteBuffer : Buffer<byte>, IManagedByteBuffer
{
public ManagedByteBuffer(byte[] data, int length, ArrayPool<byte> sourcePool)
: base(data, length, sourcePool)
{
}
/// <inheritdoc />
public byte[] Array => this.Data;
}
}
}

2
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs

@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Memory
/// This is the default. Should be good for most use cases.
/// </summary>
/// <returns>The memory manager.</returns>
public static ArrayPoolMemoryAllocator CreateDefault()
public static new ArrayPoolMemoryAllocator CreateDefault()
{
return new ArrayPoolMemoryAllocator(
DefaultMaxPooledBufferSizeInBytes,

25
src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs

@ -10,6 +10,7 @@ namespace SixLabors.ImageSharp.Memory
/// <summary>
/// Implements <see cref="MemoryAllocator"/> by allocating memory from <see cref="ArrayPool{T}"/>.
/// </summary>
[Obsolete("ArrayPoolMemoryAllocator is obsolete. Use MemoryAllocator.CreateDefault() instead.")]
public sealed partial class ArrayPoolMemoryAllocator : MemoryAllocator
{
private readonly int maxArraysPerBucketNormalPool;
@ -136,7 +137,7 @@ namespace SixLabors.ImageSharp.Memory
byte[] byteArray = pool.Rent(bufferSizeInBytes);
var buffer = new Buffer<T>(byteArray, length, pool);
if (options == AllocationOptions.Clean)
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
@ -144,27 +145,7 @@ namespace SixLabors.ImageSharp.Memory
return buffer;
}
/// <inheritdoc />
public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
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 static int GetLargeBufferThresholdInBytes(int maxPoolSizeInBytes) => maxPoolSizeInBytes / 4;
[MethodImpl(InliningOptions.ColdPath)]
private static void ThrowInvalidAllocationException<T>(int length, int max) =>

18
src/ImageSharp/Memory/Allocators/IManagedByteBuffer.cs

@ -1,18 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Buffers;
namespace SixLabors.ImageSharp.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; }
}
}

5
src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs

@ -2,11 +2,12 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Wraps an array as an <see cref="IManagedByteBuffer"/> instance.
/// Wraps an array as an <see cref="MemoryManager{T}"/> instance.
/// </summary>
/// <inheritdoc />
internal class BasicArrayBuffer<T> : ManagedBufferBase<T>
@ -57,4 +58,4 @@ namespace SixLabors.ImageSharp.Memory.Internals
return this.Array;
}
}
}
}

20
src/ImageSharp/Memory/Allocators/Internals/BasicByteBuffer.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.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)
{
}
}
}

115
src/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs

@ -0,0 +1,115 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// Port of BCL internal utility:
// https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs
#if NETCORE31COMPATIBLE
using System;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Schedules a callback roughly every gen 2 GC (you may see a Gen 0 an Gen 1 but only once)
/// (We can fix this by capturing the Gen 2 count at startup and testing, but I mostly don't care)
/// </summary>
internal sealed class Gen2GcCallback : CriticalFinalizerObject
{
private readonly Func<bool> callback0;
private readonly Func<object, bool> callback1;
private GCHandle weakTargetObj;
private Gen2GcCallback(Func<bool> callback)
{
this.callback0 = callback;
}
private Gen2GcCallback(Func<object, bool> callback, object targetObj)
{
this.callback1 = callback;
this.weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak);
}
~Gen2GcCallback()
{
if (this.weakTargetObj.IsAllocated)
{
// Check to see if the target object is still alive.
object targetObj = this.weakTargetObj.Target;
if (targetObj == null)
{
// The target object is dead, so this callback object is no longer needed.
this.weakTargetObj.Free();
return;
}
// Execute the callback method.
try
{
if (!this.callback1(targetObj))
{
// If the callback returns false, this callback object is no longer needed.
this.weakTargetObj.Free();
return;
}
}
catch
{
// Ensure that we still get a chance to resurrect this object, even if the callback throws an exception.
#if DEBUG
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
throw;
#endif
}
}
else
{
// Execute the callback method.
try
{
if (!this.callback0())
{
// If the callback returns false, this callback object is no longer needed.
return;
}
}
catch
{
// Ensure that we still get a chance to resurrect this object, even if the callback throws an exception.
#if DEBUG
// Except in DEBUG, as we really shouldn't be hitting any exceptions here.
throw;
#endif
}
}
// Resurrect ourselves by re-registering for finalization.
GC.ReRegisterForFinalize(this);
}
/// <summary>
/// Schedule 'callback' to be called in the next GC. If the callback returns true it is
/// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop.
/// </summary>
public static void Register(Func<bool> callback)
{
// Create a unreachable object that remembers the callback function and target object.
_ = new Gen2GcCallback(callback);
}
/// <summary>
/// Schedule 'callback' to be called in the next GC. If the callback returns true it is
/// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop.
///
/// NOTE: This callback will be kept alive until either the callback function returns false,
/// or the target object dies.
/// </summary>
public static void Register(Func<object, bool> callback, object targetObj)
{
// Create a unreachable object that remembers the callback function and target object.
_ = new Gen2GcCallback(callback, targetObj);
}
}
}
#endif

5
src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory.Internals
@ -23,7 +24,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
this.pinHandle = GCHandle.Alloc(this.GetPinnableObject(), GCHandleType.Pinned);
}
void* ptr = (void*)this.pinHandle.AddrOfPinnedObject();
void* ptr = Unsafe.Add<T>((void*)this.pinHandle.AddrOfPinnedObject(), elementIndex);
return new MemoryHandle(ptr, this.pinHandle);
}
@ -42,4 +43,4 @@ namespace SixLabors.ImageSharp.Memory.Internals
/// <returns>The pinnable <see cref="object"/>.</returns>
protected abstract object GetPinnableObject();
}
}
}

40
src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>
where T : struct
{
private readonly int lengthInBytes;
private byte[] array;
public SharedArrayPoolBuffer(int lengthInElements)
{
this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>();
this.array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes);
}
~SharedArrayPoolBuffer() => this.Dispose(false);
protected override void Dispose(bool disposing)
{
if (this.array == null)
{
return;
}
ArrayPool<byte>.Shared.Return(this.array);
this.array = null;
}
public override Span<T> GetSpan() => MemoryMarshal.Cast<byte, T>(this.array.AsSpan(0, this.lengthInBytes));
protected override object GetPinnableObject() => this.array;
}
}

89
src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.Buffer{T}.cs

@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal partial class UniformUnmanagedMemoryPool
{
public unsafe class Buffer<T> : MemoryManager<T>
where T : struct
{
private UniformUnmanagedMemoryPool pool;
private readonly int length;
public Buffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length)
{
this.pool = pool;
this.BufferHandle = bufferHandle;
this.length = length;
}
private void* Pointer => (void*)this.BufferHandle.DangerousGetHandle();
protected UnmanagedMemoryHandle BufferHandle { get; private set; }
public override Span<T> GetSpan() => new Span<T>(this.Pointer, this.length);
/// <inheritdoc />
public override MemoryHandle Pin(int elementIndex = 0)
{
// Will be released in Unpin
bool unused = false;
this.BufferHandle.DangerousAddRef(ref unused);
void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex);
return new MemoryHandle(pbData);
}
/// <inheritdoc />
public override void Unpin() => this.BufferHandle.DangerousRelease();
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (this.pool == null)
{
return;
}
this.pool.Return(this.BufferHandle);
this.pool = null;
this.BufferHandle = null;
}
internal void MarkDisposed()
{
this.pool = null;
this.BufferHandle = null;
}
}
public class FinalizableBuffer<T> : Buffer<T>
where T : struct
{
public FinalizableBuffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length)
: base(pool, bufferHandle, length)
{
bufferHandle.AssignedToNewOwner();
}
~FinalizableBuffer() => this.Dispose(false);
protected override void Dispose(bool disposing)
{
if (!disposing && this.BufferHandle != null)
{
// We need to prevent handle finalization here.
// See comments on UnmanagedMemoryHandle.Resurrect()
this.BufferHandle.Resurrect();
}
base.Dispose(disposing);
}
}
}
}

323
src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs

@ -0,0 +1,323 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Diagnostics;
using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal partial class UniformUnmanagedMemoryPool
{
private static readonly Stopwatch Stopwatch = Stopwatch.StartNew();
private readonly TrimSettings trimSettings;
private UnmanagedMemoryHandle[] buffers;
private int index;
private Timer trimTimer;
private long lastTrimTimestamp;
public UniformUnmanagedMemoryPool(int bufferLength, int capacity)
: this(bufferLength, capacity, TrimSettings.Default)
{
}
public UniformUnmanagedMemoryPool(int bufferLength, int capacity, TrimSettings trimSettings)
{
this.trimSettings = trimSettings;
this.Capacity = capacity;
this.BufferLength = bufferLength;
this.buffers = new UnmanagedMemoryHandle[capacity];
if (trimSettings.Enabled)
{
// Invoke the timer callback more frequently, than trimSettings.TrimPeriodMilliseconds,
// and also invoke it on Gen 2 GC.
// We are checking in the callback if enough time passed since the last trimming. If not, we do nothing.
this.trimTimer = new Timer(
s => ((UniformUnmanagedMemoryPool)s)?.Trim(),
this,
this.trimSettings.TrimPeriodMilliseconds / 4,
this.trimSettings.TrimPeriodMilliseconds / 4);
#if NETCORE31COMPATIBLE
Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this);
#endif
}
}
public int BufferLength { get; }
public int Capacity { get; }
public UnmanagedMemoryHandle Rent(AllocationOptions allocationOptions = AllocationOptions.None)
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
// Avoid taking the lock if the pool is released or is over limit:
if (buffersLocal == null || this.index == buffersLocal.Length)
{
return null;
}
UnmanagedMemoryHandle array;
lock (buffersLocal)
{
// Check again after taking the lock:
if (this.buffers == null || this.index == buffersLocal.Length)
{
return null;
}
array = buffersLocal[this.index];
buffersLocal[this.index++] = null;
}
if (array == null)
{
array = new UnmanagedMemoryHandle(this.BufferLength);
}
if (allocationOptions.Has(AllocationOptions.Clean))
{
this.GetSpan(array).Clear();
}
return array;
}
public UnmanagedMemoryHandle[] Rent(int bufferCount, AllocationOptions allocationOptions = AllocationOptions.None)
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
// Avoid taking the lock if the pool is released or is over limit:
if (buffersLocal == null || this.index + bufferCount >= buffersLocal.Length + 1)
{
return null;
}
UnmanagedMemoryHandle[] result;
lock (buffersLocal)
{
// Check again after taking the lock:
if (this.buffers == null || this.index + bufferCount >= buffersLocal.Length + 1)
{
return null;
}
result = new UnmanagedMemoryHandle[bufferCount];
for (int i = 0; i < bufferCount; i++)
{
result[i] = buffersLocal[this.index];
buffersLocal[this.index++] = null;
}
}
for (int i = 0; i < result.Length; i++)
{
if (result[i] == null)
{
result[i] = new UnmanagedMemoryHandle(this.BufferLength);
}
if (allocationOptions.Has(AllocationOptions.Clean))
{
this.GetSpan(result[i]).Clear();
}
}
return result;
}
public void Return(UnmanagedMemoryHandle buffer)
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
if (buffersLocal == null)
{
buffer.Dispose();
return;
}
lock (buffersLocal)
{
// Check again after taking the lock:
if (this.buffers == null)
{
buffer.Dispose();
return;
}
if (this.index == 0)
{
ThrowReturnedMoreArraysThanRented(); // DEBUG-only exception
buffer.Dispose();
return;
}
this.buffers[--this.index] = buffer;
}
}
public void Return(Span<UnmanagedMemoryHandle> buffers)
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
if (buffersLocal == null)
{
DisposeAll(buffers);
return;
}
lock (buffersLocal)
{
// Check again after taking the lock:
if (this.buffers == null)
{
DisposeAll(buffers);
return;
}
if (this.index - buffers.Length + 1 <= 0)
{
ThrowReturnedMoreArraysThanRented();
DisposeAll(buffers);
return;
}
for (int i = buffers.Length - 1; i >= 0; i--)
{
buffersLocal[--this.index] = buffers[i];
}
}
}
public void Release()
{
this.trimTimer?.Dispose();
this.trimTimer = null;
UnmanagedMemoryHandle[] oldBuffers = Interlocked.Exchange(ref this.buffers, null);
DebugGuard.NotNull(oldBuffers, nameof(oldBuffers));
DisposeAll(oldBuffers);
}
private static void DisposeAll(Span<UnmanagedMemoryHandle> buffers)
{
foreach (UnmanagedMemoryHandle handle in buffers)
{
handle?.Dispose();
}
}
private unsafe Span<byte> GetSpan(UnmanagedMemoryHandle h) =>
new Span<byte>((byte*)h.DangerousGetHandle(), this.BufferLength);
// This indicates a bug in the library, however Return() might be called from a finalizer,
// therefore we should never throw here in production.
[Conditional("DEBUG")]
private static void ThrowReturnedMoreArraysThanRented() =>
throw new InvalidMemoryOperationException("Returned more arrays then rented");
private bool Trim()
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
if (buffersLocal == null)
{
return false;
}
bool isHighPressure = this.IsHighMemoryPressure();
if (isHighPressure)
{
return this.TrimHighPressure(buffersLocal);
}
long millisecondsSinceLastTrim = Stopwatch.ElapsedMilliseconds - this.lastTrimTimestamp;
if (millisecondsSinceLastTrim > this.trimSettings.TrimPeriodMilliseconds)
{
return this.TrimLowPressure(buffersLocal);
}
return true;
}
private bool TrimHighPressure(UnmanagedMemoryHandle[] buffersLocal)
{
lock (buffersLocal)
{
if (this.buffers == null)
{
return false;
}
// Trim all:
for (int i = this.index; i < buffersLocal.Length && buffersLocal[i] != null; i++)
{
buffersLocal[i] = null;
}
}
return true;
}
private bool TrimLowPressure(UnmanagedMemoryHandle[] arraysLocal)
{
lock (arraysLocal)
{
if (this.buffers == null)
{
return false;
}
// Count the arrays in the pool:
int retainedCount = 0;
for (int i = this.index; i < arraysLocal.Length && arraysLocal[i] != null; i++)
{
retainedCount++;
}
// Trim 'trimRate' of 'retainedCount':
int trimCount = (int)Math.Ceiling(retainedCount * this.trimSettings.Rate);
int trimStart = this.index + retainedCount - 1;
int trimStop = this.index + retainedCount - trimCount;
for (int i = trimStart; i >= trimStop; i--)
{
arraysLocal[i].Dispose();
arraysLocal[i] = null;
}
this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds;
}
return true;
}
private bool IsHighMemoryPressure()
{
#if NETCORE31COMPATIBLE
GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo();
return memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * this.trimSettings.HighPressureThresholdRate;
#else
// We don't have high pressure detection triggering full trimming on other platforms,
// to counterpart this, the maximum pool size is small.
return false;
#endif
}
public class TrimSettings
{
// Trim half of the retained pool buffers every minute.
public int TrimPeriodMilliseconds { get; set; } = 60_000;
public float Rate { get; set; } = 0.5f;
// Be more strict about high pressure threshold than ArrayPool<T>.Shared.
// A 32 bit process can OOM before reaching HighMemoryLoadThresholdBytes.
public float HighPressureThresholdRate { get; set; } = 0.5f;
public bool Enabled => this.Rate > 0;
public static TrimSettings Default => new TrimSettings();
}
}
}

65
src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs

@ -0,0 +1,65 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Allocates and provides an <see cref="IMemoryOwner{T}"/> implementation giving
/// access to unmanaged buffers allocated by <see cref="Marshal.AllocHGlobal(int)"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
internal sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T>
where T : struct
{
private readonly UnmanagedMemoryHandle bufferHandle;
private readonly int lengthInElements;
/// <summary>
/// Initializes a new instance of the <see cref="UnmanagedBuffer{T}"/> class.
/// </summary>
/// <param name="lengthInElements">The number of elements to allocate.</param>
public UnmanagedBuffer(int lengthInElements)
{
this.lengthInElements = lengthInElements;
this.bufferHandle = new UnmanagedMemoryHandle(lengthInElements * Unsafe.SizeOf<T>());
}
private void* Pointer => (void*)this.bufferHandle.DangerousGetHandle();
public override Span<T> GetSpan()
=> new Span<T>(this.Pointer, this.lengthInElements);
/// <inheritdoc />
public override MemoryHandle Pin(int elementIndex = 0)
{
// Will be released in Unpin
bool unused = false;
this.bufferHandle.DangerousAddRef(ref unused);
void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex);
return new MemoryHandle(pbData);
}
/// <inheritdoc />
public override void Unpin() => this.bufferHandle.DangerousRelease();
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (this.bufferHandle.IsInvalid)
{
return;
}
if (disposing)
{
this.bufferHandle.Dispose();
}
}
}
}

80
src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs

@ -0,0 +1,80 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal sealed class UnmanagedMemoryHandle : SafeHandle
{
private readonly int lengthInBytes;
private bool resurrected;
// Track allocations for testing purposes:
private static int totalOutstandingHandles;
public UnmanagedMemoryHandle(int lengthInBytes)
: base(IntPtr.Zero, true)
{
this.SetHandle(Marshal.AllocHGlobal(lengthInBytes));
this.lengthInBytes = lengthInBytes;
if (lengthInBytes > 0)
{
GC.AddMemoryPressure(lengthInBytes);
}
Interlocked.Increment(ref totalOutstandingHandles);
}
/// <summary>
/// Gets a value indicating the total outstanding handle allocations for testing purposes.
/// </summary>
internal static int TotalOutstandingHandles => totalOutstandingHandles;
/// <inheritdoc />
public override bool IsInvalid => this.handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
if (this.IsInvalid)
{
return false;
}
Marshal.FreeHGlobal(this.handle);
if (this.lengthInBytes > 0)
{
GC.RemoveMemoryPressure(this.lengthInBytes);
}
this.handle = IntPtr.Zero;
Interlocked.Decrement(ref totalOutstandingHandles);
return true;
}
/// <summary>
/// UnmanagedMemoryHandle's finalizer would release the underlying handle returning the memory to the OS.
/// We want to prevent this when a finalizable owner (buffer or MemoryGroup) is returning the handle to
/// <see cref="UniformUnmanagedMemoryPool"/> in it's finalizer.
/// Since UnmanagedMemoryHandle is CriticalFinalizable, it is guaranteed that the owner's finalizer is called first.
/// </summary>
internal void Resurrect()
{
GC.SuppressFinalize(this);
this.resurrected = true;
}
internal void AssignedToNewOwner()
{
if (this.resurrected)
{
// The handle has been resurrected
GC.ReRegisterForFinalize(this);
this.resurrected = false;
}
}
}
}

40
src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

@ -17,6 +17,21 @@ namespace SixLabors.ImageSharp.Memory
/// <returns>The length of the largest contiguous buffer that can be handled by this allocator instance.</returns>
protected internal abstract int GetBufferCapacityInBytes();
/// <summary>
/// Creates a default instance of a <see cref="MemoryAllocator"/> optimized for the executing platform.
/// </summary>
/// <returns>The <see cref="MemoryAllocator"/>.</returns>
public static MemoryAllocator CreateDefault() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(null, null);
/// <summary>
/// Creates the default <see cref="MemoryAllocator"/> using the provided options.
/// </summary>
/// <param name="options">The <see cref="MemoryAllocatorOptions"/>.</param>
/// <returns>The <see cref="MemoryAllocator"/>.</returns>
public static MemoryAllocator CreateDefault(MemoryAllocatorOptions options) =>
new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes, options.MinimumContiguousBlockBytes);
/// <summary>
/// Allocates an <see cref="IMemoryOwner{T}" />, holding a <see cref="Memory{T}"/> of length <paramref name="length"/>.
/// </summary>
@ -29,16 +44,6 @@ namespace SixLabors.ImageSharp.Memory
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>
/// <exception cref="ArgumentOutOfRangeException">When length is zero or negative.</exception>
/// <exception cref="InvalidMemoryOperationException">When length is over the capacity of the allocator.</exception>
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.
@ -46,5 +51,20 @@ namespace SixLabors.ImageSharp.Memory
public virtual void ReleaseRetainedResources()
{
}
/// <summary>
/// Allocates a <see cref="MemoryGroup{T}"/>.
/// </summary>
/// <param name="totalLength">The total length of the buffer.</param>
/// <param name="bufferAlignment">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
/// <param name="options">The <see cref="AllocationOptions"/>.</param>
/// <returns>A new <see cref="MemoryGroup{T}"/>.</returns>
/// <exception cref="InvalidMemoryOperationException">Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator.</exception>
internal virtual MemoryGroup<T> AllocateGroup<T>(
long totalLength,
int bufferAlignment,
AllocationOptions options = AllocationOptions.None)
where T : struct
=> MemoryGroup<T>.Allocate(this, totalLength, bufferAlignment, options);
}
}

57
src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs

@ -0,0 +1,57 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Defines options for creating the default <see cref="MemoryAllocator"/>.
/// </summary>
public class MemoryAllocatorOptions
{
private int? maximumPoolSizeMegabytes;
private int? minimumContiguousBlockBytes;
/// <summary>
/// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool
/// in Megabytes. <see langword="null"/> means platform default.
/// </summary>
public int? MaximumPoolSizeMegabytes
{
get => this.maximumPoolSizeMegabytes;
set
{
if (value.HasValue)
{
Guard.MustBeGreaterThanOrEqualTo(value.Value, 0, nameof(this.MaximumPoolSizeMegabytes));
}
this.maximumPoolSizeMegabytes = value;
}
}
/// <summary>
/// Gets or sets a value defining the minimum contiguous block size when allocating buffers for
/// <see cref="MemoryGroup{T}"/>, <see cref="Buffer2D{T}"/> or <see cref="Image{TPixel}"/>.
/// <see langword="null"/> means platform default.
/// </summary>
/// <remarks>
/// Overriding this value is useful for interop scenarios
/// ensuring <see cref="Image{TPixel}.TryGetSinglePixelSpan"/> succeeds.
/// </remarks>
public int? MinimumContiguousBlockBytes
{
get => this.minimumContiguousBlockBytes;
set
{
if (value.HasValue)
{
// It doesn't make sense to set this to small values in practice.
// Defining an arbitrary minimum of 65536.
Guard.MustBeGreaterThanOrEqualTo(value.Value, 65536, nameof(this.MaximumPoolSizeMegabytes));
}
this.minimumContiguousBlockBytes = value;
}
}
}
}

8
src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

@ -21,13 +21,5 @@ namespace SixLabors.ImageSharp.Memory
return new BasicArrayBuffer<T>(new T[length]);
}
/// <inheritdoc />
public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
return new BasicByteBuffer(new byte[length]);
}
}
}

149
src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

@ -0,0 +1,149 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
{
internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocator
{
private const int OneMegabyte = 1 << 20;
private const int DefaultContiguousPoolBlockSizeBytes = 4 * OneMegabyte;
private const int DefaultNonPoolBlockSizeBytes = 32 * OneMegabyte;
private readonly int sharedArrayPoolThresholdInBytes;
private readonly int poolBufferSizeInBytes;
private readonly int poolCapacity;
private UniformUnmanagedMemoryPool pool;
private readonly UnmanagedMemoryAllocator nonPoolAllocator;
public UniformUnmanagedMemoryPoolMemoryAllocator(int? maxPoolSizeMegabytes, int? minimumContiguousBlockBytes)
: this(
minimumContiguousBlockBytes.HasValue ? minimumContiguousBlockBytes.Value : DefaultContiguousPoolBlockSizeBytes,
maxPoolSizeMegabytes.HasValue ? (long)maxPoolSizeMegabytes.Value * OneMegabyte : GetDefaultMaxPoolSizeBytes(),
minimumContiguousBlockBytes.HasValue ? Math.Max(minimumContiguousBlockBytes.Value, DefaultNonPoolBlockSizeBytes) : DefaultNonPoolBlockSizeBytes)
{
}
public UniformUnmanagedMemoryPoolMemoryAllocator(
int poolBufferSizeInBytes,
long maxPoolSizeInBytes,
int unmanagedBufferSizeInBytes)
: this(
OneMegabyte,
poolBufferSizeInBytes,
maxPoolSizeInBytes,
unmanagedBufferSizeInBytes)
{
}
// Internal constructor allowing to change the shared array pool threshold for testing purposes.
internal UniformUnmanagedMemoryPoolMemoryAllocator(
int sharedArrayPoolThresholdInBytes,
int poolBufferSizeInBytes,
long maxPoolSizeInBytes,
int unmanagedBufferSizeInBytes)
{
this.sharedArrayPoolThresholdInBytes = sharedArrayPoolThresholdInBytes;
this.poolBufferSizeInBytes = poolBufferSizeInBytes;
this.poolCapacity = (int)(maxPoolSizeInBytes / poolBufferSizeInBytes);
this.pool = new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity);
this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes);
}
/// <inheritdoc />
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes;
/// <inheritdoc />
public override IMemoryOwner<T> Allocate<T>(
int length,
AllocationOptions options = AllocationOptions.None)
{
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
int lengthInBytes = length * Unsafe.SizeOf<T>();
if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
var buffer = new SharedArrayPoolBuffer<T>(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return buffer;
}
if (lengthInBytes <= this.poolBufferSizeInBytes)
{
UnmanagedMemoryHandle array = this.pool.Rent(options);
if (array != null)
{
return new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, length);
}
}
return this.nonPoolAllocator.Allocate<T>(length, options);
}
/// <inheritdoc />
internal override MemoryGroup<T> AllocateGroup<T>(
long totalLength,
int bufferAlignment,
AllocationOptions options = AllocationOptions.None)
{
long totalLengthInBytes = totalLength * Unsafe.SizeOf<T>();
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
var buffer = new SharedArrayPoolBuffer<T>((int)totalLength);
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}
if (totalLengthInBytes <= this.poolBufferSizeInBytes)
{
// Optimized path renting single array from the pool
UnmanagedMemoryHandle array = this.pool.Rent(options);
if (array != null)
{
var buffer = new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, (int)totalLength);
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}
}
// Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails:
MemoryGroup<T> poolGroup = options.Has(AllocationOptions.Contiguous) ?
null :
MemoryGroup<T>.Allocate(this.pool, totalLength, bufferAlignment, options);
return poolGroup ?? MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options);
}
public override void ReleaseRetainedResources()
{
UniformUnmanagedMemoryPool oldPool = Interlocked.Exchange(
ref this.pool,
new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity));
oldPool.Release();
}
private static long GetDefaultMaxPoolSizeBytes()
{
#if NETCORE31COMPATIBLE
// On .NET Core 3.1+, determine the pool as portion of the total available memory.
// There is a bug in GC.GetGCMemoryInfo() on .NET 5 + 32 bit, making TotalAvailableMemoryBytes unreliable:
// https://github.com/dotnet/runtime/issues/55126#issuecomment-876779327
if (Environment.Is64BitProcess || !RuntimeInformation.FrameworkDescription.StartsWith(".NET 5.0"))
{
GCMemoryInfo info = GC.GetGCMemoryInfo();
return info.TotalAvailableMemoryBytes / 8;
}
#endif
// Stick to a conservative value of 128 Megabytes on other platforms and 32 bit .NET 5.0:
return 128 * OneMegabyte;
}
}
}

32
src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs

@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
{
internal class UnmanagedMemoryAllocator : MemoryAllocator
{
private readonly int bufferCapacityInBytes;
public UnmanagedMemoryAllocator(int bufferCapacityInBytes)
{
this.bufferCapacityInBytes = bufferCapacityInBytes;
}
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{
var buffer = new UnmanagedBuffer<T>(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return buffer;
}
}
}

86
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -19,8 +19,6 @@ namespace SixLabors.ImageSharp.Memory
public sealed class Buffer2D<T> : IDisposable
where T : struct
{
private Memory<T> cachedMemory = default;
/// <summary>
/// Initializes a new instance of the <see cref="Buffer2D{T}"/> class.
/// </summary>
@ -32,11 +30,6 @@ namespace SixLabors.ImageSharp.Memory
this.FastMemoryGroup = memoryGroup;
this.Width = width;
this.Height = height;
if (memoryGroup.Count == 1)
{
this.cachedMemory = memoryGroup[0];
}
}
/// <summary>
@ -89,11 +82,7 @@ namespace SixLabors.ImageSharp.Memory
/// <summary>
/// Disposes the <see cref="Buffer2D{T}"/> instance
/// </summary>
public void Dispose()
{
this.FastMemoryGroup.Dispose();
this.cachedMemory = default;
}
public void Dispose() => this.FastMemoryGroup.Dispose();
/// <summary>
/// Gets a <see cref="Span{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
@ -111,39 +100,14 @@ namespace SixLabors.ImageSharp.Memory
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
return this.cachedMemory.Length > 0
? this.cachedMemory.Span.Slice(y * this.Width, this.Width)
: this.GetRowMemorySlow(y).Span;
return this.GetRowMemoryCore(y).Span;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal ref T GetElementUnsafe(int x, int y)
{
if (this.cachedMemory.Length > 0)
{
Span<T> span = this.cachedMemory.Span;
ref T start = ref MemoryMarshal.GetReference(span);
return ref Unsafe.Add(ref start, (y * this.Width) + x);
}
return ref this.GetElementSlow(x, y);
}
/// <summary>
/// Gets a <see cref="Memory{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
/// This method is intended for internal use only, since it does not use the indirection provided by
/// <see cref="MemoryGroupView{T}"/>.
/// </summary>
/// <param name="y">The y (row) coordinate.</param>
/// <returns>The <see cref="Span{T}"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
internal Memory<T> GetFastRowMemory(int y)
{
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
return this.cachedMemory.Length > 0
? this.cachedMemory.Slice(y * this.Width, this.Width)
: this.GetRowMemorySlow(y);
Span<T> span = this.GetRowMemoryCore(y).Span;
return ref span[x];
}
/// <summary>
@ -168,11 +132,7 @@ namespace SixLabors.ImageSharp.Memory
/// Thrown when the backing group is discontiguous.
/// </exception>
[MethodImpl(InliningOptions.ShortMethod)]
internal Span<T> DangerousGetSingleSpan()
{
// TODO: If we need a public version of this method, we need to cache the non-fast Memory<T> of this.MemoryGroup
return this.cachedMemory.Length != 0 ? this.cachedMemory.Span : this.DangerousGetSingleSpanSlow();
}
internal Span<T> DangerousGetSingleSpan() => this.FastMemoryGroup.Single().Span;
/// <summary>
/// Gets a <see cref="Memory{T}"/> to the backing data of if the backing group consists of a single contiguous memory buffer.
@ -183,11 +143,7 @@ namespace SixLabors.ImageSharp.Memory
/// Thrown when the backing group is discontiguous.
/// </exception>
[MethodImpl(InliningOptions.ShortMethod)]
internal Memory<T> DangerousGetSingleMemory()
{
// TODO: If we need a public version of this method, we need to cache the non-fast Memory<T> of this.MemoryGroup
return this.cachedMemory.Length != 0 ? this.cachedMemory : this.DangerousGetSingleMemorySlow();
}
internal Memory<T> DangerousGetSingleMemory() => this.FastMemoryGroup.Single();
/// <summary>
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
@ -195,27 +151,14 @@ namespace SixLabors.ImageSharp.Memory
/// </summary>
internal static void SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{
bool swap = MemoryGroup<T>.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup);
SwapOwnData(destination, source, swap);
MemoryGroup<T>.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup);
SwapOwnData(destination, source);
}
[MethodImpl(InliningOptions.ColdPath)]
private Memory<T> GetRowMemorySlow(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width);
[MethodImpl(InliningOptions.ColdPath)]
private Memory<T> DangerousGetSingleMemorySlow() => this.FastMemoryGroup.Single();
[MethodImpl(InliningOptions.ColdPath)]
private Span<T> DangerousGetSingleSpanSlow() => this.FastMemoryGroup.Single().Span;
[MethodImpl(InliningOptions.ColdPath)]
private ref T GetElementSlow(int x, int y)
{
Span<T> span = this.GetRowMemorySlow(y).Span;
return ref span[x];
}
[MethodImpl(InliningOptions.ShortMethod)]
private Memory<T> GetRowMemoryCore(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width);
private static void SwapOwnData(Buffer2D<T> a, Buffer2D<T> b, bool swapCachedMemory)
private static void SwapOwnData(Buffer2D<T> a, Buffer2D<T> b)
{
Size aSize = a.Size();
Size bSize = b.Size();
@ -225,13 +168,6 @@ namespace SixLabors.ImageSharp.Memory
a.Width = bSize.Width;
a.Height = bSize.Height;
if (swapCachedMemory)
{
Memory<T> aCached = a.cachedMemory;
a.cachedMemory = b.cachedMemory;
b.cachedMemory = aCached;
}
}
}
}

7
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs

@ -50,9 +50,12 @@ namespace SixLabors.ImageSharp.Memory
return ((IList<Memory<T>>)this.source).GetEnumerator();
}
public override void Dispose()
protected override void Dispose(bool disposing)
{
this.View.Invalidate();
if (disposing)
{
this.View.Invalidate();
}
}
}
}

74
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs

@ -6,6 +6,7 @@ using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
{
@ -17,6 +18,9 @@ namespace SixLabors.ImageSharp.Memory
public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>>
{
private IMemoryOwner<T>[] memoryOwners;
private byte[][] pooledArrays;
private UniformUnmanagedMemoryPool unmanagedMemoryPool;
private UnmanagedMemoryHandle[] pooledHandles;
public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable)
: base(bufferLength, totalLength)
@ -26,6 +30,15 @@ namespace SixLabors.ImageSharp.Memory
this.View = new MemoryGroupView<T>(this);
}
public Owned(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] pooledArrays, int bufferLength, long totalLength, int sizeOfLastBuffer)
: this(CreateBuffers(pool, pooledArrays, bufferLength, sizeOfLastBuffer), bufferLength, totalLength, true)
{
this.pooledHandles = pooledArrays;
this.unmanagedMemoryPool = pool;
}
~Owned() => this.Dispose(false);
public bool Swappable { get; }
private bool IsDisposed => this.memoryOwners == null;
@ -49,6 +62,23 @@ namespace SixLabors.ImageSharp.Memory
}
}
private static IMemoryOwner<T>[] CreateBuffers(
UniformUnmanagedMemoryPool pool,
UnmanagedMemoryHandle[] pooledBuffers,
int bufferLength,
int sizeOfLastBuffer)
{
var result = new IMemoryOwner<T>[pooledBuffers.Length];
for (int i = 0; i < pooledBuffers.Length - 1; i++)
{
pooledBuffers[i].AssignedToNewOwner();
result[i] = new UniformUnmanagedMemoryPool.Buffer<T>(pool, pooledBuffers[i], bufferLength);
}
result[result.Length - 1] = new UniformUnmanagedMemoryPool.Buffer<T>(pool, pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer);
return result;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator()
@ -63,7 +93,7 @@ namespace SixLabors.ImageSharp.Memory
return this.memoryOwners.Select(mo => mo.Memory).GetEnumerator();
}
public override void Dispose()
protected override void Dispose(bool disposing)
{
if (this.IsDisposed)
{
@ -72,13 +102,37 @@ namespace SixLabors.ImageSharp.Memory
this.View.Invalidate();
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
if (this.unmanagedMemoryPool != null)
{
memoryOwner.Dispose();
this.unmanagedMemoryPool.Return(this.pooledHandles);
if (!disposing)
{
foreach (UnmanagedMemoryHandle handle in this.pooledHandles)
{
// We need to prevent handle finalization here.
// See comments on UnmanagedMemoryHandle.Resurrect()
handle.Resurrect();
}
}
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
{
((UniformUnmanagedMemoryPool.Buffer<T>)memoryOwner).MarkDisposed();
}
}
else if (disposing)
{
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
{
memoryOwner.Dispose();
}
}
this.memoryOwners = null;
this.IsValid = false;
this.pooledArrays = null;
this.unmanagedMemoryPool = null;
this.pooledHandles = null;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -91,10 +145,7 @@ namespace SixLabors.ImageSharp.Memory
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(MemoryGroup<T>));
}
private static void ThrowObjectDisposedException() => throw new ObjectDisposedException(nameof(MemoryGroup<T>));
internal static void SwapContents(Owned a, Owned b)
{
@ -104,14 +155,23 @@ namespace SixLabors.ImageSharp.Memory
IMemoryOwner<T>[] tempOwners = a.memoryOwners;
long tempTotalLength = a.TotalLength;
int tempBufferLength = a.BufferLength;
byte[][] tempPooledArrays = a.pooledArrays;
UniformUnmanagedMemoryPool tempUnmangedPool = a.unmanagedMemoryPool;
UnmanagedMemoryHandle[] tempPooledHandles = a.pooledHandles;
a.memoryOwners = b.memoryOwners;
a.TotalLength = b.TotalLength;
a.BufferLength = b.BufferLength;
a.pooledArrays = b.pooledArrays;
a.unmanagedMemoryPool = b.unmanagedMemoryPool;
a.pooledHandles = b.pooledHandles;
b.memoryOwners = tempOwners;
b.TotalLength = tempTotalLength;
b.BufferLength = tempBufferLength;
b.pooledArrays = tempPooledArrays;
b.unmanagedMemoryPool = tempUnmangedPool;
b.pooledHandles = tempPooledHandles;
a.View.Invalidate();
b.View.Invalidate();

110
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs

@ -6,6 +6,7 @@ using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
{
@ -44,7 +45,13 @@ namespace SixLabors.ImageSharp.Memory
public abstract Memory<T> this[int index] { get; }
/// <inheritdoc />
public abstract void Dispose();
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected abstract void Dispose(bool disposing);
/// <inheritdoc />
public abstract MemoryGroupEnumerator<T> GetEnumerator();
@ -67,44 +74,47 @@ namespace SixLabors.ImageSharp.Memory
/// Creates a new memory group, allocating it's buffers with the provided allocator.
/// </summary>
/// <param name="allocator">The <see cref="MemoryAllocator"/> to use.</param>
/// <param name="totalLength">The total length of the buffer.</param>
/// <param name="bufferAlignment">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
/// <param name="totalLengthInElements">The total length of the buffer.</param>
/// <param name="bufferAlignmentInElements">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
/// <param name="options">The <see cref="AllocationOptions"/>.</param>
/// <returns>A new <see cref="MemoryGroup{T}"/>.</returns>
/// <exception cref="InvalidMemoryOperationException">Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator.</exception>
public static MemoryGroup<T> Allocate(
MemoryAllocator allocator,
long totalLength,
int bufferAlignment,
long totalLengthInElements,
int bufferAlignmentInElements,
AllocationOptions options = AllocationOptions.None)
{
int bufferCapacityInBytes = options.Has(AllocationOptions.Contiguous) ?
int.MaxValue :
allocator.GetBufferCapacityInBytes();
Guard.NotNull(allocator, nameof(allocator));
Guard.MustBeGreaterThanOrEqualTo(totalLength, 0, nameof(totalLength));
Guard.MustBeGreaterThanOrEqualTo(bufferAlignment, 0, nameof(bufferAlignment));
Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements));
Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements));
int blockCapacityInElements = allocator.GetBufferCapacityInBytes() / ElementSize;
int blockCapacityInElements = bufferCapacityInBytes / ElementSize;
if (bufferAlignment > blockCapacityInElements)
if (bufferAlignmentInElements > blockCapacityInElements)
{
throw new InvalidMemoryOperationException(
$"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignment}.");
$"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignmentInElements}.");
}
if (totalLength == 0)
if (totalLengthInElements == 0)
{
var buffers0 = new IMemoryOwner<T>[1] { allocator.Allocate<T>(0, options) };
return new Owned(buffers0, 0, 0, true);
}
int numberOfAlignedSegments = blockCapacityInElements / bufferAlignment;
int bufferLength = numberOfAlignedSegments * bufferAlignment;
if (totalLength > 0 && totalLength < bufferLength)
int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements;
int bufferLength = numberOfAlignedSegments * bufferAlignmentInElements;
if (totalLengthInElements > 0 && totalLengthInElements < bufferLength)
{
bufferLength = (int)totalLength;
bufferLength = (int)totalLengthInElements;
}
int sizeOfLastBuffer = (int)(totalLength % bufferLength);
long bufferCount = totalLength / bufferLength;
int sizeOfLastBuffer = (int)(totalLengthInElements % bufferLength);
long bufferCount = totalLengthInElements / bufferLength;
if (sizeOfLastBuffer == 0)
{
@ -126,7 +136,71 @@ namespace SixLabors.ImageSharp.Memory
buffers[buffers.Length - 1] = allocator.Allocate<T>(sizeOfLastBuffer, options);
}
return new Owned(buffers, bufferLength, totalLength, true);
return new Owned(buffers, bufferLength, totalLengthInElements, true);
}
public static MemoryGroup<T> CreateContiguous(IMemoryOwner<T> buffer, bool clear)
{
if (clear)
{
buffer.GetSpan().Clear();
}
int length = buffer.Memory.Length;
var buffers = new IMemoryOwner<T>[1] { buffer };
return new Owned(buffers, length, length, true);
}
public static MemoryGroup<T> Allocate(
UniformUnmanagedMemoryPool pool,
long totalLengthInElements,
int bufferAlignmentInElements,
AllocationOptions options = AllocationOptions.None)
{
Guard.NotNull(pool, nameof(pool));
Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements));
Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements));
int blockCapacityInElements = pool.BufferLength / ElementSize;
if (bufferAlignmentInElements > blockCapacityInElements)
{
return null;
}
if (totalLengthInElements == 0)
{
throw new InvalidMemoryOperationException("Allocating 0 length buffer from UniformByteArrayPool is disallowed");
}
int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements;
int bufferLength = numberOfAlignedSegments * bufferAlignmentInElements;
if (totalLengthInElements > 0 && totalLengthInElements < bufferLength)
{
bufferLength = (int)totalLengthInElements;
}
int sizeOfLastBuffer = (int)(totalLengthInElements % bufferLength);
int bufferCount = (int)(totalLengthInElements / bufferLength);
if (sizeOfLastBuffer == 0)
{
sizeOfLastBuffer = bufferLength;
}
else
{
bufferCount++;
}
UnmanagedMemoryHandle[] arrays = pool.Rent(bufferCount, options);
if (arrays == null)
{
// Pool is full
return null;
}
return new Owned(pool, arrays, bufferLength, totalLengthInElements, sizeOfLastBuffer);
}
public static MemoryGroup<T> Wrap(params Memory<T>[] source)

17
src/ImageSharp/Memory/MemoryAllocatorExtensions.cs

@ -83,22 +83,5 @@ namespace SixLabors.ImageSharp.Memory
int length = (width * pixelSizeInBytes) + paddingInBytes;
return memoryAllocator.Allocate<byte>(length);
}
/// <summary>
/// Allocates a <see cref="MemoryGroup{T}"/>.
/// </summary>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use.</param>
/// <param name="totalLength">The total length of the buffer.</param>
/// <param name="bufferAlignment">The expected alignment (eg. to make sure image rows fit into single buffers).</param>
/// <param name="options">The <see cref="AllocationOptions"/>.</param>
/// <returns>A new <see cref="MemoryGroup{T}"/>.</returns>
/// <exception cref="InvalidMemoryOperationException">Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator.</exception>
internal static MemoryGroup<T> AllocateGroup<T>(
this MemoryAllocator memoryAllocator,
long totalLength,
int bufferAlignment,
AllocationOptions options = AllocationOptions.None)
where T : struct
=> MemoryGroup<T>.Allocate(memoryAllocator, totalLength, bufferAlignment, options);
}
}

2
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -51,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.sourceLength = sourceLength;
this.DestinationLength = destinationLength;
this.MaxDiameter = (radius * 2) + 1;
this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, AllocationOptions.Clean);
this.data = memoryAllocator.Allocate2D<float>(this.MaxDiameter, bufferHeight, AllocationOptions.Clean | AllocationOptions.Contiguous);
this.pinHandle = this.data.DangerousGetSingleMemory().Pin();
this.kernels = new ResizeKernel[destinationLength];
this.tempValues = new double[this.MaxDiameter];

4
tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj

@ -38,9 +38,9 @@
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Colourful" />
<PackageReference Include="NetVips"/>
<PackageReference Include="NetVips" />
<PackageReference Include="NetVips.Native" />
<PackageReference Include="PhotoSauce.MagicScaler"/>
<PackageReference Include="PhotoSauce.MagicScaler" />
<PackageReference Include="Pfim" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Condition="'$(IsOSX)'=='true'" />
<PackageReference Include="SharpZipLib" />

25
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs

@ -3,6 +3,7 @@
using System;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
{
@ -16,6 +17,11 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
// private const JpegKind Filter = JpegKind.Progressive;
private const JpegKind Filter = JpegKind.Any;
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
private ArrayPoolMemoryAllocator arrayPoolMemoryAllocator;
#pragma warning restore CS0618
private MemoryAllocator defaultMemoryAllocator;
[GlobalSetup]
public void Setup()
{
@ -26,6 +32,11 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
};
Console.WriteLine($"ImageCount: {this.runner.ImageCount} Filter: {Filter}");
this.runner.Init();
this.defaultMemoryAllocator = Configuration.Default.MemoryAllocator;
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
this.arrayPoolMemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault();
#pragma warning restore CS0618
}
private void ForEachImage(Action<string> action, int maxDegreeOfParallelism)
@ -48,7 +59,19 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
[Benchmark]
[ArgumentsSource(nameof(ParallelismValues))]
public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism);
public void ImageSharp_DefaultMemoryAllocator(int maxDegreeOfParallelism)
{
Configuration.Default.MemoryAllocator = this.defaultMemoryAllocator;
this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism);
}
[Benchmark]
[ArgumentsSource(nameof(ParallelismValues))]
public void ImageSharp_ArrayPoolMemoryAllocator(int maxDegreeOfParallelism)
{
Configuration.Default.MemoryAllocator = this.arrayPoolMemoryAllocator;
this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism);
}
[Benchmark]
[ArgumentsSource(nameof(ParallelismValues))]

2
tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs

@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave
public int MaxDegreeOfParallelism { get; set; } = -1;
public JpegKind Filter { get; set; }
public JpegKind Filter { get; set; } = JpegKind.Any;
private static readonly string[] ProgressiveFiles =
{

1
tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj

@ -44,6 +44,7 @@
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Condition="'$(IsOSX)'=='true'" />
<PackageReference Include="SkiaSharp" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="CommandLineParser" Version="2.8.0" />
</ItemGroup>
<ItemGroup>

241
tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs

@ -3,29 +3,107 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime;
using System.Text;
using System.Threading;
using CommandLine;
using SixLabors.ImageSharp.Benchmarks.LoadResizeSave;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
{
// See ImageSharp.Benchmarks/LoadResizeSave/README.md
internal class LoadResizeSaveParallelMemoryStress
{
private readonly LoadResizeSaveStressRunner benchmarks;
private LoadResizeSaveParallelMemoryStress()
{
this.benchmarks = new LoadResizeSaveStressRunner()
this.Benchmarks = new LoadResizeSaveStressRunner()
{
// MaxDegreeOfParallelism = 10,
// Filter = JpegKind.Baseline
};
this.benchmarks.Init();
this.Benchmarks.Init();
}
public LoadResizeSaveStressRunner Benchmarks { get; }
public static void Run(string[] args)
{
var options = CommandLineOptions.Parse(args);
var lrs = new LoadResizeSaveParallelMemoryStress();
if (options != null)
{
lrs.Benchmarks.MaxDegreeOfParallelism = options.MaxDegreeOfParallelism;
}
Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}");
Stopwatch timer;
if (options == null || !options.ImageSharp)
{
RunBenchmarkSwitcher(lrs, out timer);
}
else
{
Console.WriteLine("Running ImageSharp with options:");
Console.WriteLine(options.ToString());
Configuration.Default.MemoryAllocator = options.CreateMemoryAllocator();
timer = Stopwatch.StartNew();
try
{
for (int i = 0; i < options.RepeatCount; i++)
{
lrs.ImageSharpBenchmarkParallel();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
timer.Stop();
if (options.ReleaseRetainedResourcesAtEnd)
{
Configuration.Default.MemoryAllocator.ReleaseRetainedResources();
}
for (int i = 0; i < options.FinalGcCount; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
Thread.Sleep(1000);
}
}
var stats = new Stats(timer, lrs.Benchmarks.TotalProcessedMegapixels);
Console.WriteLine(stats.GetMarkdown());
if (options?.FileOutput != null)
{
PrintFileOutput(options.FileOutput, stats);
}
if (options != null && options.PauseAtEnd)
{
Console.WriteLine("Press ENTER");
Console.ReadLine();
}
}
private double TotalProcessedMegapixels => this.benchmarks.TotalProcessedMegapixels;
private static void PrintFileOutput(string fileOutput, Stats stats)
{
string[] ss = fileOutput.Split(';');
string fileName = ss[0];
string content = ss[1]
.Replace("TotalSeconds", stats.TotalSeconds.ToString(CultureInfo.InvariantCulture))
.Replace("EOL", Environment.NewLine, StringComparison.OrdinalIgnoreCase);
File.AppendAllText(fileName, content);
}
public static void Run()
private static void RunBenchmarkSwitcher(LoadResizeSaveParallelMemoryStress lrs, out Stopwatch timer)
{
Console.WriteLine(@"Choose a library for image resizing stress test:
@ -41,48 +119,34 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
if (key < ConsoleKey.D1 || key > ConsoleKey.D6)
{
Console.WriteLine("Unrecognized command.");
return;
Environment.Exit(-1);
}
try
{
var lrs = new LoadResizeSaveParallelMemoryStress();
timer = Stopwatch.StartNew();
Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}");
Console.WriteLine($"Running with MaxDegreeOfParallelism={lrs.benchmarks.MaxDegreeOfParallelism} ...");
var timer = Stopwatch.StartNew();
switch (key)
{
case ConsoleKey.D1:
lrs.SystemDrawingBenchmarkParallel();
break;
case ConsoleKey.D2:
lrs.ImageSharpBenchmarkParallel();
break;
case ConsoleKey.D3:
lrs.MagicScalerBenchmarkParallel();
break;
case ConsoleKey.D4:
lrs.SkiaBitmapBenchmarkParallel();
break;
case ConsoleKey.D5:
lrs.NetVipsBenchmarkParallel();
break;
case ConsoleKey.D6:
lrs.MagickBenchmarkParallel();
break;
}
timer.Stop();
var stats = new Stats(timer, lrs.TotalProcessedMegapixels);
Console.WriteLine("Done. TotalProcessedMegapixels: " + lrs.TotalProcessedMegapixels);
Console.WriteLine(stats.GetMarkdown());
}
catch (Exception ex)
switch (key)
{
Console.WriteLine(ex.ToString());
case ConsoleKey.D1:
lrs.SystemDrawingBenchmarkParallel();
break;
case ConsoleKey.D2:
lrs.ImageSharpBenchmarkParallel();
break;
case ConsoleKey.D3:
lrs.MagicScalerBenchmarkParallel();
break;
case ConsoleKey.D4:
lrs.SkiaBitmapBenchmarkParallel();
break;
case ConsoleKey.D5:
lrs.NetVipsBenchmarkParallel();
break;
case ConsoleKey.D6:
lrs.MagickBenchmarkParallel();
break;
}
timer.Stop();
}
private struct Stats
@ -125,18 +189,95 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
}
}
private void ForEachImage(Action<string> action) => this.benchmarks.ForEachImageParallel(action);
private enum AllocatorKind
{
Classic,
Unmanaged
}
private class CommandLineOptions
{
[Option('i', "imagesharp", Required = false, Default = false)]
public bool ImageSharp { get; set; }
[Option('a', "allocator", Required = false, Default = AllocatorKind.Unmanaged)]
public AllocatorKind Allocator { get; set; }
[Option('m', "max-contiguous", Required = false, Default = 4)]
public int MaxContiguousPoolBufferMegaBytes { get; set; } = 4;
[Option('s', "poolsize", Required = false, Default = 4096)]
public int MaxPoolSizeMegaBytes { get; set; } = 4096;
[Option('u', "max-unmg", Required = false, Default = 32)]
public int MaxCapacityOfUnmanagedBuffersMegaBytes { get; set; } = 32;
[Option('p', "parallelism", Required = false, Default = -1)]
public int MaxDegreeOfParallelism { get; set; } = -1;
[Option('r', "repeat-count", Required = false, Default = 1)]
public int RepeatCount { get; set; } = 1;
// This is to test trimming and virtual memory decommit
[Option('g', "final-gc-count", Required = false, Default = 0)]
public int FinalGcCount { get; set; }
[Option('e', "release-at-end", Required = false, Default = false)]
public bool ReleaseRetainedResourcesAtEnd { get; set; }
[Option('w', "pause", Required = false, Default = false)]
public bool PauseAtEnd { get; set; }
[Option('f', "file", Required = false, Default = null)]
public string FileOutput { get; set; }
public static CommandLineOptions Parse(string[] args)
{
CommandLineOptions result = null;
Parser.Default.ParseArguments<CommandLineOptions>(args).WithParsed(o =>
{
result = o;
});
return result;
}
public override string ToString() =>
$"p({this.MaxDegreeOfParallelism})_i({this.ImageSharp})_a({this.Allocator})_m({this.MaxContiguousPoolBufferMegaBytes})_s({this.MaxPoolSizeMegaBytes})_u({this.MaxCapacityOfUnmanagedBuffersMegaBytes})_r({this.RepeatCount})_g({this.FinalGcCount})_e({this.ReleaseRetainedResourcesAtEnd})";
public MemoryAllocator CreateMemoryAllocator()
{
switch (this.Allocator)
{
case AllocatorKind.Classic:
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
return ArrayPoolMemoryAllocator.CreateDefault();
#pragma warning restore CS0618
case AllocatorKind.Unmanaged:
return new UniformUnmanagedMemoryPoolMemoryAllocator(
1024 * 1024,
(int)B(this.MaxContiguousPoolBufferMegaBytes),
B(this.MaxPoolSizeMegaBytes),
(int)B(this.MaxCapacityOfUnmanagedBuffersMegaBytes));
default:
throw new ArgumentOutOfRangeException();
}
}
private static long B(double megaBytes) => (long)(megaBytes * 1024 * 1024);
}
private void ForEachImage(Action<string> action) => this.Benchmarks.ForEachImageParallel(action);
private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.benchmarks.SystemDrawingResize);
private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SystemDrawingResize);
private void ImageSharpBenchmarkParallel() => this.ForEachImage(this.benchmarks.ImageSharpResize);
private void ImageSharpBenchmarkParallel() => this.ForEachImage(this.Benchmarks.ImageSharpResize);
private void MagickBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagickResize);
private void MagickBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagickResize);
private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagicScalerResize);
private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagicScalerResize);
private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaBitmapResize);
private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapResize);
private void NetVipsBenchmarkParallel() => this.ForEachImage(this.benchmarks.NetVipsResize);
private void NetVipsBenchmarkParallel() => this.ForEachImage(this.Benchmarks.NetVipsResize);
}
}

5
tests/ImageSharp.Tests.ProfilingSandbox/Program.cs

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Threading;
using SixLabors.ImageSharp.Memory.Internals;
using SixLabors.ImageSharp.Tests.Formats.Jpg;
using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations;
using SixLabors.ImageSharp.Tests.ProfilingBenchmarks;
@ -31,7 +33,8 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox
/// </param>
public static void Main(string[] args)
{
LoadResizeSaveParallelMemoryStress.Run();
LoadResizeSaveParallelMemoryStress.Run(args);
// TrimPoolTest();
// RunJpegEncoderProfilingTests();
// RunJpegColorProfilingTests();
// RunDecodeJpegProfilingTests();

2
tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs

@ -98,8 +98,6 @@ namespace SixLabors.ImageSharp.Tests.Formats
public void QuantizeImageShouldPreserveMaximumColorPrecision<TPixel>(TestImageProvider<TPixel> provider, string quantizerName)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.Configuration.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling();
IQuantizer quantizer = GetQuantizer(quantizerName);
using (Image<TPixel> image = provider.GetImage())

4
tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs

@ -46,7 +46,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
public static readonly string[] AllTestJpegs = BaselineTestJpegs.Concat(ProgressiveTestJpegs).ToArray();
[Theory(Skip = "Debug only, enable manually!")]
//[Theory]
[WithFileCollection(nameof(AllTestJpegs), PixelTypes.Rgba32)]
public void Decoder_ParseStream_SaveSpectralResult<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
@ -126,7 +125,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]");
averageDifference += average;
totalDifference += total;
tolerance += libJpegComponent.SpectralBlocks.DangerousGetSingleSpan().Length;
Size s = libJpegComponent.SpectralBlocks.Size();
tolerance += s.Width * s.Height;
}
averageDifference /= componentCount;

2
tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs

@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Compression
var stream = new BufferedReadStream(Configuration.Default, new MemoryStream(inputData));
var buffer = new byte[expectedResult.Length];
using var decompressor = new PackBitsTiffCompression(new ArrayPoolMemoryAllocator(), default, default);
using var decompressor = new PackBitsTiffCompression(MemoryAllocator.CreateDefault(), default, default);
decompressor.Decompress(stream, 0, (uint)inputData.Length, buffer);
Assert.Equal(expectedResult, buffer);

2
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs

@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
[Trait("Format", "Tiff")]
public class TiffEncoderHeaderTests
{
private static readonly MemoryAllocator MemoryAllocator = new ArrayPoolMemoryAllocator();
private static readonly MemoryAllocator MemoryAllocator = MemoryAllocator.CreateDefault();
private static readonly Configuration Configuration = Configuration.Default;
private static readonly ITiffEncoderOptions Options = new TiffEncoder();

2
tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

@ -361,7 +361,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers
in operation);
// Assert:
TestImageExtensions.CompareBuffers(expected.DangerousGetSingleSpan(), actual.DangerousGetSingleSpan());
TestImageExtensions.CompareBuffers(expected, actual);
}
}

6
tests/ImageSharp.Tests/Image/ImageFrameTests.cs

@ -16,8 +16,12 @@ namespace SixLabors.ImageSharp.Tests
private void LimitBufferCapacity(int bufferCapacityInBytes)
{
var allocator = (ArrayPoolMemoryAllocator)this.configuration.MemoryAllocator;
// TODO: Create a test-only MemoryAllocator for this
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
var allocator = ArrayPoolMemoryAllocator.CreateDefault();
#pragma warning restore CS0618
allocator.BufferCapacityInBytes = bufferCapacityInBytes;
this.configuration.MemoryAllocator = allocator;
}
[Theory]

6
tests/ImageSharp.Tests/Image/ImageTests.cs

@ -99,8 +99,12 @@ namespace SixLabors.ImageSharp.Tests
private void LimitBufferCapacity(int bufferCapacityInBytes)
{
var allocator = (ArrayPoolMemoryAllocator)this.configuration.MemoryAllocator;
// TODO: Create a test-only MemoryAllocator for this
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
var allocator = ArrayPoolMemoryAllocator.CreateDefault();
#pragma warning restore CS0618
allocator.BufferCapacityInBytes = bufferCapacityInBytes;
this.configuration.MemoryAllocator = allocator;
}
[Theory]

3
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -7,6 +7,7 @@
<Platforms>AnyCPU;x64;x86</Platforms>
<RootNamespace>SixLabors.ImageSharp.Tests</RootNamespace>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
<DefineConstants Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)','netcoreapp3.1'))">$(DefineConstants);NETCORE31COMPATIBLE</DefineConstants>
</PropertyGroup>
<Choose>
@ -21,7 +22,7 @@
</PropertyGroup>
</Otherwise>
</Choose>
<ItemGroup>
<InternalsVisibleTo Include="ImageSharp.Tests.ProfilingSandbox" Key="$(SixLaborsPublicKey)" />
</ItemGroup>

11
tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs

@ -11,6 +11,7 @@ using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
#pragma warning disable CS0618
public class ArrayPoolMemoryAllocatorTests
{
private const int MaxPooledBufferSizeInBytes = 2048;
@ -223,15 +224,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
Assert.Equal(0, buffer.Memory.Length);
}
[Theory]
[InlineData(-1)]
public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
{
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
this.LocalFixture.MemoryAllocator.AllocateManagedByteBuffer(length));
Assert.Equal("length", ex.ParamName);
}
private class MemoryAllocatorFixture
{
public ArrayPoolMemoryAllocator MemoryAllocator { get; set; } =
@ -268,4 +260,5 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
}
}
#pragma warning restore CS0618
}

61
tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs

@ -96,8 +96,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[MemberData(nameof(LengthValues))]
public void CanAllocateCleanBuffer_byte(int desiredLength)
{
this.TestCanAllocateCleanBuffer<byte>(desiredLength, false);
this.TestCanAllocateCleanBuffer<byte>(desiredLength, true);
this.TestCanAllocateCleanBuffer<byte>(desiredLength);
}
[Theory]
@ -114,30 +113,14 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
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)
private void TestCanAllocateCleanBuffer<T>(int desiredLength)
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))
using (IMemoryOwner<T> buffer = this.MemoryAllocator.Allocate<T>(desiredLength, AllocationOptions.Clean))
{
Assert.True(buffer.GetSpan().SequenceEqual(expected));
}
@ -155,14 +138,13 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[MemberData(nameof(LengthValues))]
public void SpanPropertyIsAlwaysTheSame_byte(int desiredLength)
{
this.TestSpanPropertyIsAlwaysTheSame<byte>(desiredLength, false);
this.TestSpanPropertyIsAlwaysTheSame<byte>(desiredLength, true);
this.TestSpanPropertyIsAlwaysTheSame<byte>(desiredLength);
}
private void TestSpanPropertyIsAlwaysTheSame<T>(int desiredLength, bool testManagedByteBuffer = false)
private void TestSpanPropertyIsAlwaysTheSame<T>(int desiredLength)
where T : struct
{
using (IMemoryOwner<T> buffer = this.Allocate<T>(desiredLength, AllocationOptions.None, testManagedByteBuffer))
using (IMemoryOwner<T> buffer = this.MemoryAllocator.Allocate<T>(desiredLength, AllocationOptions.None))
{
ref T a = ref MemoryMarshal.GetReference(buffer.GetSpan());
ref T b = ref MemoryMarshal.GetReference(buffer.GetSpan());
@ -184,14 +166,13 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[MemberData(nameof(LengthValues))]
public void WriteAndReadElements_byte(int desiredLength)
{
this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), false);
this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), true);
this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1));
}
private void TestWriteAndReadElements<T>(int desiredLength, Func<int, T> getExpectedValue, bool testManagedByteBuffer = false)
private void TestWriteAndReadElements<T>(int desiredLength, Func<int, T> getExpectedValue)
where T : struct
{
using (IMemoryOwner<T> buffer = this.Allocate<T>(desiredLength, AllocationOptions.None, testManagedByteBuffer))
using (IMemoryOwner<T> buffer = this.MemoryAllocator.Allocate<T>(desiredLength))
{
var expectedVals = new T[buffer.Length()];
@ -214,8 +195,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[MemberData(nameof(LengthValues))]
public void IndexingSpan_WhenOutOfRange_Throws_byte(int desiredLength)
{
this.TestIndexOutOfRangeShouldThrow<byte>(desiredLength, false);
this.TestIndexOutOfRangeShouldThrow<byte>(desiredLength, true);
this.TestIndexOutOfRangeShouldThrow<byte>(desiredLength);
}
[Theory]
@ -232,12 +212,12 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
this.TestIndexOutOfRangeShouldThrow<CustomStruct>(desiredLength);
}
private T TestIndexOutOfRangeShouldThrow<T>(int desiredLength, bool testManagedByteBuffer = false)
private T TestIndexOutOfRangeShouldThrow<T>(int desiredLength)
where T : struct, IEquatable<T>
{
var dummy = default(T);
using (IMemoryOwner<T> buffer = this.Allocate<T>(desiredLength, AllocationOptions.None, testManagedByteBuffer))
using (IMemoryOwner<T> buffer = this.MemoryAllocator.Allocate<T>(desiredLength))
{
Assert.ThrowsAny<Exception>(
() =>
@ -264,23 +244,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
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()
{

10
tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs

@ -28,17 +28,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
Assert.Equal("length", ex.ParamName);
}
[Theory]
[InlineData(-1)]
public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
{
ArgumentOutOfRangeException ex = Assert.Throws<ArgumentOutOfRangeException>(() => this.MemoryAllocator.AllocateManagedByteBuffer(length));
Assert.Equal("length", ex.ParamName);
}
[StructLayout(LayoutKind.Explicit, Size = 512)]
private struct BigStruct
{
}
}
}
}

101
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs

@ -0,0 +1,101 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public partial class UniformUnmanagedMemoryPoolTests
{
[CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)]
public class NonParallelTests
{
}
[Collection(nameof(NonParallelTests))]
public class Trim
{
[Fact]
public void TrimPeriodElapsed_TrimsHalfOfUnusedArrays()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 5_000 };
var pool = new UniformUnmanagedMemoryPool(128, 256, trimSettings);
UnmanagedMemoryHandle[] a = pool.Rent(64);
UnmanagedMemoryHandle[] b = pool.Rent(64);
pool.Return(a);
Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles);
Thread.Sleep(15_000);
// We expect at least 2 Trim actions, first trim 32, then 16 arrays.
// 128 - 32 - 16 = 80
Assert.True(
UnmanagedMemoryHandle.TotalOutstandingHandles <= 80,
$"UnmanagedMemoryHandle.TotalOutstandingHandles={UnmanagedMemoryHandle.TotalOutstandingHandles} > 80");
pool.Return(b);
}
}
#if NETCORE31COMPATIBLE
public static readonly bool Is32BitProcess = !Environment.Is64BitProcess;
private static readonly List<byte[]> PressureArrays = new List<byte[]>();
[ConditionalFact(nameof(Is32BitProcess))]
public static void GC_Collect_OnHighLoad_TrimsEntirePool()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
Assert.False(Environment.Is64BitProcess);
const int OneMb = 1024 * 1024;
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { HighPressureThresholdRate = 0.2f };
GCMemoryInfo memInfo = GC.GetGCMemoryInfo();
int highLoadThreshold = (int)(memInfo.HighMemoryLoadThresholdBytes / OneMb);
highLoadThreshold = (int)(trimSettings.HighPressureThresholdRate * highLoadThreshold);
var pool = new UniformUnmanagedMemoryPool(OneMb, 16, trimSettings);
pool.Return(pool.Rent(16));
Assert.Equal(16, UnmanagedMemoryHandle.TotalOutstandingHandles);
for (int i = 0; i < highLoadThreshold; i++)
{
byte[] array = new byte[OneMb];
TouchPage(array);
PressureArrays.Add(array);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers(); // The pool should be fully trimmed after this point
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
static void TouchPage(byte[] b)
{
uint size = (uint)b.Length;
const uint pageSize = 4096;
uint numPages = size / pageSize;
for (uint i = 0; i < numPages; i++)
{
b[i * pageSize] = (byte)(i % 256);
}
}
}
}
#endif
}
}
}

292
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs

@ -0,0 +1,292 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public partial class UniformUnmanagedMemoryPoolTests
{
private readonly ITestOutputHelper output;
public UniformUnmanagedMemoryPoolTests(ITestOutputHelper output)
{
this.output = output;
}
private static unsafe Span<byte> GetSpan(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) =>
new Span<byte>((void*)h.DangerousGetHandle(), pool.BufferLength);
[Theory]
[InlineData(3, 11)]
[InlineData(7, 4)]
public void Constructor_InitializesProperties(int arrayLength, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(arrayLength, capacity);
Assert.Equal(arrayLength, pool.BufferLength);
Assert.Equal(capacity, pool.Capacity);
}
[Theory]
[InlineData(1, 3)]
[InlineData(8, 10)]
public void Rent_SingleBuffer_ReturnsCorrectBuffer(int length, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(length, capacity);
for (int i = 0; i < capacity; i++)
{
UnmanagedMemoryHandle h = pool.Rent();
CheckBuffer(length, pool, h);
}
}
[Fact]
public void Return_DoesNotDeallocateMemory()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var pool = new UniformUnmanagedMemoryPool(16, 16);
UnmanagedMemoryHandle a = pool.Rent();
UnmanagedMemoryHandle[] b = pool.Rent(2);
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles);
pool.Return(a);
pool.Return(b);
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
private static void CheckBuffer(int length, UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h)
{
Assert.NotNull(h);
Assert.False(h.IsClosed);
Span<byte> span = GetSpan(pool, h);
span.Fill(123);
byte[] expected = new byte[length];
expected.AsSpan().Fill(123);
Assert.True(span.SequenceEqual(expected));
}
[Theory]
[InlineData(1, 1)]
[InlineData(1, 5)]
[InlineData(42, 7)]
[InlineData(5, 10)]
public void Rent_MultiBuffer_ReturnsCorrectBuffers(int length, int bufferCount)
{
var pool = new UniformUnmanagedMemoryPool(length, 10);
UnmanagedMemoryHandle[] handles = pool.Rent(bufferCount);
Assert.NotNull(handles);
Assert.Equal(bufferCount, handles.Length);
foreach (UnmanagedMemoryHandle h in handles)
{
CheckBuffer(length, pool, h);
}
}
[Fact]
public void Rent_MultipleTimesWithoutReturn_ReturnsDifferentHandles()
{
var pool = new UniformUnmanagedMemoryPool(128, 10);
UnmanagedMemoryHandle[] a = pool.Rent(2);
UnmanagedMemoryHandle b = pool.Rent();
Assert.NotEqual(a[0].DangerousGetHandle(), a[1].DangerousGetHandle());
Assert.NotEqual(a[0].DangerousGetHandle(), b.DangerousGetHandle());
Assert.NotEqual(a[1].DangerousGetHandle(), b.DangerousGetHandle());
}
[Theory]
[InlineData(4, 2, 10)]
[InlineData(5, 1, 6)]
[InlineData(12, 4, 12)]
public void RentReturnRent_SameBuffers(int totalCount, int rentUnit, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(128, capacity);
var allHandles = new HashSet<UnmanagedMemoryHandle>();
var handleUnits = new List<UnmanagedMemoryHandle[]>();
UnmanagedMemoryHandle[] handles;
for (int i = 0; i < totalCount; i += rentUnit)
{
handles = pool.Rent(rentUnit);
Assert.NotNull(handles);
handleUnits.Add(handles);
foreach (UnmanagedMemoryHandle array in handles)
{
allHandles.Add(array);
}
}
foreach (UnmanagedMemoryHandle[] arrayUnit in handleUnits)
{
if (arrayUnit.Length == 1)
{
// Test single-array return:
pool.Return(arrayUnit.Single());
}
else
{
pool.Return(arrayUnit);
}
}
handles = pool.Rent(totalCount);
Assert.NotNull(handles);
foreach (UnmanagedMemoryHandle array in handles)
{
Assert.Contains(array, allHandles);
}
}
[Fact]
public void Rent_SingleBuffer_OverCapacity_ReturnsNull()
{
var pool = new UniformUnmanagedMemoryPool(7, 1000);
Assert.NotNull(pool.Rent(1000));
Assert.Null(pool.Rent());
}
[Theory]
[InlineData(0, 6, 5)]
[InlineData(5, 1, 5)]
[InlineData(4, 7, 10)]
public void Rent_MultiBuffer_OverCapacity_ReturnsNull(int initialRent, int attempt, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(128, capacity);
Assert.NotNull(pool.Rent(initialRent));
Assert.Null(pool.Rent(attempt));
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(5, 1, 6)]
[InlineData(4, 7, 11)]
[InlineData(3, 3, 7)]
public void Rent_MultiBuff_BelowCapacity_Succeeds(int initialRent, int attempt, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(128, capacity);
Assert.NotNull(pool.Rent(initialRent));
Assert.NotNull(pool.Rent(attempt));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Release_SubsequentRentReturnsNull(bool multiple)
{
var pool = new UniformUnmanagedMemoryPool(16, 16);
pool.Rent(); // Dummy rent
pool.Release();
if (multiple)
{
UnmanagedMemoryHandle b = pool.Rent();
Assert.Null(b);
}
else
{
UnmanagedMemoryHandle[] b = pool.Rent(2);
Assert.Null(b);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Release_SubsequentReturnClosesHandle(bool multiple)
{
var pool = new UniformUnmanagedMemoryPool(16, 16);
if (multiple)
{
UnmanagedMemoryHandle[] b = pool.Rent(2);
pool.Release();
Assert.False(b[0].IsClosed);
Assert.False(b[1].IsClosed);
pool.Return(b);
Assert.True(b[0].IsClosed);
Assert.True(b[1].IsClosed);
}
else
{
UnmanagedMemoryHandle b = pool.Rent();
pool.Release();
Assert.False(b.IsClosed);
pool.Return(b);
Assert.True(b.IsClosed);
}
}
[Fact]
public void Release_ShouldFreeRetainedMemory()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var pool = new UniformUnmanagedMemoryPool(16, 16);
UnmanagedMemoryHandle a = pool.Rent();
UnmanagedMemoryHandle[] b = pool.Rent(2);
pool.Return(a);
pool.Return(b);
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles);
pool.Release();
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
[Fact]
public void RentReturn_IsThreadSafe()
{
int count = Environment.ProcessorCount * 200;
var pool = new UniformUnmanagedMemoryPool(8, count);
var rnd = new Random(0);
Parallel.For(0, Environment.ProcessorCount, (int i) =>
{
var allArrays = new List<UnmanagedMemoryHandle>();
int pauseAt = rnd.Next(100);
for (int j = 0; j < 100; j++)
{
UnmanagedMemoryHandle[] data = pool.Rent(2);
GetSpan(pool, data[0]).Fill((byte)i);
GetSpan(pool, data[1]).Fill((byte)i);
allArrays.Add(data[0]);
allArrays.Add(data[1]);
if (j == pauseAt)
{
Thread.Sleep(15);
}
}
Span<byte> expected = new byte[8];
expected.Fill((byte)i);
foreach (UnmanagedMemoryHandle array in allArrays)
{
Assert.True(expected.SequenceEqual(GetSpan(pool, array)));
pool.Return(new[] { array });
}
});
}
}
}

289
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

@ -0,0 +1,289 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Memory.Internals;
using SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public class UniformUnmanagedPoolMemoryAllocatorTests
{
public class BufferTests1 : BufferTestSuite
{
private static MemoryAllocator CreateMemoryAllocator() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes: 1024,
poolBufferSizeInBytes: 2048,
maxPoolSizeInBytes: 2048 * 4,
unmanagedBufferSizeInBytes: 4096);
public BufferTests1()
: base(CreateMemoryAllocator())
{
}
}
public class BufferTests2 : BufferTestSuite
{
private static MemoryAllocator CreateMemoryAllocator() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes: 512,
poolBufferSizeInBytes: 1024,
maxPoolSizeInBytes: 1024 * 4,
unmanagedBufferSizeInBytes: 2048);
public BufferTests2()
: base(CreateMemoryAllocator())
{
}
}
public static TheoryData<object, int, int, int, int, long, int, int, int, int> AllocateData =
new TheoryData<object, int, int, int, int, long, int, int, int, int>()
{
{ default(S4), 16, 256, 256, 1024, 64, 64, 1, -1, 64 },
{ default(S4), 16, 256, 256, 1024, 256, 256, 1, -1, 256 },
{ default(S4), 16, 256, 256, 1024, 250, 256, 1, -1, 250 },
{ default(S4), 16, 256, 256, 1024, 248, 250, 1, -1, 248 },
{ default(S4), 16, 1024, 2048, 4096, 512, 256, 2, 256, 256 },
{ default(S4), 16, 1024, 1024, 1024, 511, 256, 2, 256, 255 },
};
[Theory]
[MemberData(nameof(AllocateData))]
public void AllocateGroup_BufferSizesAreCorrect<T>(
T dummy,
int sharedArrayPoolThresholdInBytes,
int maxContiguousPoolBufferInBytes,
int maxPoolSizeInBytes,
int maxCapacityOfUnmanagedBuffers,
long allocationLengthInElements,
int bufferAlignmentInElements,
int expectedNumberOfBuffers,
int expectedBufferSize,
int expectedSizeOfLastBuffer)
where T : struct
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes,
maxContiguousPoolBufferInBytes,
maxPoolSizeInBytes,
maxCapacityOfUnmanagedBuffers);
using MemoryGroup<T> g = allocator.AllocateGroup<T>(allocationLengthInElements, bufferAlignmentInElements);
MemoryGroupTests.Allocate.ValidateAllocateMemoryGroup(
expectedNumberOfBuffers,
expectedBufferSize,
expectedSizeOfLastBuffer,
g);
}
[Fact]
public void AllocateGroup_MultipleTimes_ExceedPoolLimit()
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(
64,
128,
1024,
1024);
var groups = new List<MemoryGroup<S4>>();
for (int i = 0; i < 16; i++)
{
int lengthInElements = 128 / Unsafe.SizeOf<S4>();
MemoryGroup<S4> g = allocator.AllocateGroup<S4>(lengthInElements, 32);
MemoryGroupTests.Allocate.ValidateAllocateMemoryGroup(1, -1, lengthInElements, g);
groups.Add(g);
}
foreach (MemoryGroup<S4> g in groups)
{
g.Dispose();
}
}
[Theory]
[InlineData(512)]
[InlineData(2048)]
[InlineData(8192)]
[InlineData(65536)]
public void AllocateGroup_OptionsContiguous_AllocatesContiguousBuffer(int lengthInBytes)
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(
128,
1024,
2048,
4096);
int length = lengthInBytes / Unsafe.SizeOf<S4>();
using MemoryGroup<S4> g = allocator.AllocateGroup<S4>(length, 32, AllocationOptions.Contiguous);
Assert.Equal(length, g.BufferLength);
Assert.Equal(length, g.TotalLength);
Assert.Equal(1, g.Count);
}
[Fact]
public void MemoryAllocator_CreateDefault_WithoutOptions_AllocatesDiscontiguousMemory()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var allocator = MemoryAllocator.CreateDefault();
long sixteenMegabytes = 16 * (1 << 20);
// Should allocate 4 times 4MB discontiguos blocks:
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(sixteenMegabytes, 1024);
Assert.Equal(4, g.Count);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MemoryAllocator_CreateDefault_WithOptions_CanForceContiguousAllocation(bool poolAllocation)
{
RemoteExecutor.Invoke(RunTest, poolAllocation.ToString()).Dispose();
static void RunTest(string poolAllocationStr)
{
int fortyEightMegabytes = 48 * (1 << 20);
var allocator = MemoryAllocator.CreateDefault(new MemoryAllocatorOptions()
{
MaximumPoolSizeMegabytes = bool.Parse(poolAllocationStr) ? 64 : 0,
MinimumContiguousBlockBytes = fortyEightMegabytes
});
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(fortyEightMegabytes, 1024);
Assert.Equal(1, g.Count);
Assert.Equal(fortyEightMegabytes, g.TotalLength);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void BufferDisposal_ReturnsToPool(bool shared)
{
RemoteExecutor.Invoke(RunTest, shared.ToString()).Dispose();
static void RunTest(string sharedStr)
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024);
IMemoryOwner<byte> buffer0 = allocator.Allocate<byte>(bool.Parse(sharedStr) ? 300 : 600);
buffer0.GetSpan()[0] = 42;
buffer0.Dispose();
using IMemoryOwner<byte> buffer1 = allocator.Allocate<byte>(bool.Parse(sharedStr) ? 300 : 600);
Assert.Equal(42, buffer1.GetSpan()[0]);
}
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MemoryGroupDisposal_ReturnsToPool(bool shared)
{
RemoteExecutor.Invoke(RunTest, shared.ToString()).Dispose();
static void RunTest(string sharedStr)
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024);
MemoryGroup<byte> g0 = allocator.AllocateGroup<byte>(bool.Parse(sharedStr) ? 300 : 600, 100);
g0.Single().Span[0] = 42;
g0.Dispose();
using MemoryGroup<byte> g1 = allocator.AllocateGroup<byte>(bool.Parse(sharedStr) ? 300 : 600, 100);
Assert.Equal(42, g1.Single().Span[0]);
}
}
[Fact]
public void ReleaseRetainedResources_ShouldFreePooledMemory()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(128, 512, 16 * 512, 1024);
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(2048, 128);
g.Dispose();
Assert.Equal(4, UnmanagedMemoryHandle.TotalOutstandingHandles);
allocator.ReleaseRetainedResources();
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
[Fact]
public void ReleaseRetainedResources_DoesNotFreeOutstandingBuffers()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(128, 512, 16 * 512, 1024);
IMemoryOwner<byte> b = allocator.Allocate<byte>(256);
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(2048, 128);
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles);
allocator.ReleaseRetainedResources();
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles);
b.Dispose();
g.Dispose();
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
[Theory]
[InlineData(300)]
[InlineData(600)]
[InlineData(1200)]
public void MemoryGroupFinalizer_ReturnsToPool(int length)
{
// RunTest(length.ToString());
RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose();
static void RunTest(string lengthStr)
{
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024);
int lengthInner = int.Parse(lengthStr);
AllocateGroupAndForget(allocator, lengthInner);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
AllocateGroupAndForget(allocator, lengthInner, true);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
using MemoryGroup<byte> g = allocator.AllocateGroup<byte>(lengthInner, 100);
Assert.Equal(42, g.First().Span[0]);
}
}
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
{
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(length, 100);
if (check)
{
Assert.Equal(42, g.First().Span[0]);
}
g.First().Span[0] = 42;
if (length < 512)
{
// For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
// repeat rental to make sure per-core buckets are also utilized.
MemoryGroup<byte> g1 = allocator.AllocateGroup<byte>(length, 100);
g1.First().Span[0] = 42;
}
}
}
}

184
tests/ImageSharp.Tests/Memory/Allocators/UnmanagedMemoryHandleTests.cs

@ -0,0 +1,184 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public class UnmanagedMemoryHandleTests
{
[Fact]
public unsafe void Constructor_AllocatesReadWriteMemory()
{
using var h = new UnmanagedMemoryHandle(128);
Assert.False(h.IsClosed);
Assert.False(h.IsInvalid);
byte* ptr = (byte*)h.DangerousGetHandle();
for (int i = 0; i < 128; i++)
{
ptr[i] = (byte)i;
}
for (int i = 0; i < 128; i++)
{
Assert.Equal((byte)i, ptr[i]);
}
}
[Fact]
public void Dispose_ClosesHandle()
{
var h = new UnmanagedMemoryHandle(128);
h.Dispose();
Assert.True(h.IsClosed);
Assert.True(h.IsInvalid);
}
[Theory]
[InlineData(1)]
[InlineData(13)]
public void CreateDispose_TracksAllocations(int count)
{
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose();
static void RunTest(string countStr)
{
int countInner = int.Parse(countStr);
var l = new List<UnmanagedMemoryHandle>();
for (int i = 0; i < countInner; i++)
{
Assert.Equal(i, UnmanagedMemoryHandle.TotalOutstandingHandles);
var h = new UnmanagedMemoryHandle(42);
Assert.Equal(i + 1, UnmanagedMemoryHandle.TotalOutstandingHandles);
l.Add(h);
}
for (int i = 0; i < countInner; i++)
{
Assert.Equal(countInner - i, UnmanagedMemoryHandle.TotalOutstandingHandles);
l[i].Dispose();
Assert.Equal(countInner - i - 1, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
}
[Theory]
[InlineData(2)]
[InlineData(12)]
public void CreateFinalize_TracksAllocations(int count)
{
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose();
static void RunTest(string countStr)
{
int countInner = int.Parse(countStr);
List<UnmanagedMemoryHandle> l = FillList(countInner);
l.RemoveRange(0, l.Count / 2);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(countInner / 2, l.Count); // This is here to prevent eager finalization of the list's elements
Assert.Equal(countInner / 2, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
static List<UnmanagedMemoryHandle> FillList(int countInner)
{
var l = new List<UnmanagedMemoryHandle>();
for (int i = 0; i < countInner; i++)
{
var h = new UnmanagedMemoryHandle(42);
l.Add(h);
}
return l;
}
}
[Fact]
public void Resurrect_PreventsFinalization()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
AllocateResurrect();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
static void AllocateResurrect()
{
var h = new UnmanagedMemoryHandle(42);
h.Resurrect();
}
}
private static UnmanagedMemoryHandle resurrectedHandle;
private class HandleOwner
{
private UnmanagedMemoryHandle handle;
public HandleOwner(UnmanagedMemoryHandle handle) => this.handle = handle;
~HandleOwner()
{
resurrectedHandle = this.handle;
this.handle.Resurrect();
}
}
[Fact]
public void AssignedToNewOwner_ReRegistersForFinalization()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
AllocateAndForget();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
GC.Collect();
GC.WaitForPendingFinalizers();
VerifyResurrectedHandle(true);
GC.Collect();
GC.WaitForPendingFinalizers();
VerifyResurrectedHandle(false);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
static void AllocateAndForget()
{
_ = new HandleOwner(new UnmanagedMemoryHandle(42));
}
static void VerifyResurrectedHandle(bool reAssign)
{
Assert.NotNull(resurrectedHandle);
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
Assert.False(resurrectedHandle.IsClosed);
Assert.False(resurrectedHandle.IsInvalid);
resurrectedHandle.AssignedToNewOwner();
if (reAssign)
{
_ = new HandleOwner(resurrectedHandle);
}
resurrectedHandle = null;
}
}
}
}

2
tests/ImageSharp.Tests/Memory/Buffer2DTests.cs

@ -227,13 +227,11 @@ namespace SixLabors.ImageSharp.Tests.Memory
int actual1 = dest.GetRowSpan(0)[0];
int actual2 = dest.GetRowSpan(0)[0];
int actual3 = dest.GetSafeRowMemory(0).Span[0];
int actual4 = dest.GetFastRowMemory(0).Span[0];
int actual5 = dest[0, 0];
Assert.Equal(1, actual1);
Assert.Equal(1, actual2);
Assert.Equal(1, actual3);
Assert.Equal(1, actual4);
Assert.Equal(1, actual5);
}

121
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs

@ -2,10 +2,15 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
{
@ -39,6 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
{ default(S4), 50, 7, 21, 3, 7, 7 },
{ default(S4), 50, 7, 23, 4, 7, 2 },
{ default(S4), 50, 6, 13, 2, 12, 1 },
{ default(S4), 1024, 20, 800, 4, 240, 80 },
{ default(short), 200, 50, 49, 1, 49, 49 },
{ default(short), 200, 50, 1, 1, 1, 1 },
@ -47,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
[Theory]
[MemberData(nameof(AllocateData))]
public void BufferSizesAreCorrect<T>(
public void Allocate_FromMemoryAllocator_BufferSizesAreCorrect<T>(
T dummy,
int bufferCapacity,
int bufferAlignment,
@ -63,6 +69,94 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
using var g = MemoryGroup<T>.Allocate(this.MemoryAllocator, totalLength, bufferAlignment);
// Assert:
ValidateAllocateMemoryGroup(expectedNumberOfBuffers, expectedBufferSize, expectedSizeOfLastBuffer, g);
}
[Theory]
[MemberData(nameof(AllocateData))]
public void Allocate_FromPool_BufferSizesAreCorrect<T>(
T dummy,
int bufferCapacity,
int bufferAlignment,
long totalLength,
int expectedNumberOfBuffers,
int expectedBufferSize,
int expectedSizeOfLastBuffer)
where T : struct
{
if (totalLength == 0)
{
// Invalid case for UniformByteArrayPool allocations
return;
}
var pool = new UniformUnmanagedMemoryPool(bufferCapacity, expectedNumberOfBuffers);
// Act:
using var g = MemoryGroup<T>.Allocate(pool, totalLength, bufferAlignment);
// Assert:
ValidateAllocateMemoryGroup(expectedNumberOfBuffers, expectedBufferSize, expectedSizeOfLastBuffer, g);
}
private static unsafe Span<byte> GetSpan(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) =>
new Span<byte>((void*)h.DangerousGetHandle(), pool.BufferLength);
[Theory]
[InlineData(AllocationOptions.None)]
[InlineData(AllocationOptions.Clean)]
public void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options)
{
var pool = new UniformUnmanagedMemoryPool(10, 5);
UnmanagedMemoryHandle[] buffers = pool.Rent(5);
foreach (UnmanagedMemoryHandle b in buffers)
{
GetSpan(pool, b).Fill(42);
}
pool.Return(buffers);
using var g = MemoryGroup<byte>.Allocate(pool, 50, 10, options);
Span<byte> expected = stackalloc byte[10];
expected.Fill((byte)(options == AllocationOptions.Clean ? 0 : 42));
foreach (Memory<byte> memory in g)
{
Assert.True(expected.SequenceEqual(memory.Span));
}
}
[Theory]
[InlineData(64, 4, 60, 240, false)]
[InlineData(64, 4, 60, 244, true)]
public void Allocate_FromPool_AroundLimit(
int bufferCapacityBytes,
int poolCapacity,
int alignmentBytes,
int requestBytes,
bool shouldReturnNull)
{
var pool = new UniformUnmanagedMemoryPool(bufferCapacityBytes, poolCapacity);
int alignmentElements = alignmentBytes / Unsafe.SizeOf<S4>();
int requestElements = requestBytes / Unsafe.SizeOf<S4>();
using var g = MemoryGroup<S4>.Allocate(pool, requestElements, alignmentElements);
if (shouldReturnNull)
{
Assert.Null(g);
}
else
{
Assert.NotNull(g);
}
}
internal static void ValidateAllocateMemoryGroup<T>(
int expectedNumberOfBuffers,
int expectedBufferSize,
int expectedSizeOfLastBuffer,
MemoryGroup<T> g)
where T : struct
{
Assert.Equal(expectedNumberOfBuffers, g.Count);
if (expectedBufferSize >= 0)
@ -123,6 +217,31 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
Assert.Equal(expectedBlockCount, this.MemoryAllocator.ReturnLog.Count);
Assert.True(bufferHashes.SetEquals(this.MemoryAllocator.ReturnLog.Select(l => l.HashCodeOfBuffer)));
}
[Theory]
[InlineData(128)]
[InlineData(1024)]
public void Allocate_OptionsContiguous_AllocatesContiguousBuffer(int lengthInBytes)
{
this.MemoryAllocator.BufferCapacityInBytes = 256;
int length = lengthInBytes / Unsafe.SizeOf<S4>();
using var g = MemoryGroup<S4>.Allocate(this.MemoryAllocator, length, 32, AllocationOptions.Contiguous);
Assert.Equal(length, g.BufferLength);
Assert.Equal(length, g.TotalLength);
Assert.Equal(1, g.Count);
}
}
}
[StructLayout(LayoutKind.Sequential, Size = 5)]
internal struct S5
{
public override string ToString() => "S5";
}
[StructLayout(LayoutKind.Sequential, Size = 4)]
internal struct S4
{
public override string ToString() => "S4";
}
}

12
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs

@ -229,17 +229,5 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
target[k] = source[k] * 2;
}
}
[StructLayout(LayoutKind.Sequential, Size = 5)]
private struct S5
{
public override string ToString() => "S5";
}
[StructLayout(LayoutKind.Sequential, Size = 4)]
private struct S4
{
public override string ToString() => "S4";
}
}
}

2
tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs

@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Dithering
/// but it is very different because of floating point inaccuracies.
/// </summary>
private static readonly bool SkipAllDitherTests =
!TestEnvironment.Is64BitProcess && string.IsNullOrEmpty(TestEnvironment.NetCoreVersion);
!TestEnvironment.Is64BitProcess && TestEnvironment.NetCoreVersion == null;
[Theory]
[WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32)]

2
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -342,7 +342,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
// Resize_WorksWithAllResamplers_TestPattern301x1180_NearestNeighbor-300x480.png
// TODO: Should we investigate this?
bool allowHigherInaccuracy = !TestEnvironment.Is64BitProcess
&& string.IsNullOrEmpty(TestEnvironment.NetCoreVersion)
&& TestEnvironment.NetCoreVersion == null
&& sampler is NearestNeighborResampler;
var comparer = ImageComparer.TolerantPercentage(allowHigherInaccuracy ? 0.3f : 0.017f);

10
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -8,6 +8,7 @@ using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using Xunit.Abstractions;
@ -157,7 +158,14 @@ namespace SixLabors.ImageSharp.Tests
return this.LoadImage(decoder);
}
int bufferCapacity = this.Configuration.MemoryAllocator.GetBufferCapacityInBytes();
int bufferCapacity = -1;
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
if (this.Configuration.MemoryAllocator is ArrayPoolMemoryAllocator arrayPoolMemoryAllocator)
#pragma warning restore CS0618
{
bufferCapacity = arrayPoolMemoryAllocator.BufferCapacityInBytes;
}
var key = new Key(this.PixelType, this.FilePath, bufferCapacity, decoder);
Image<TPixel> cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder));

12
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs

@ -24,14 +24,14 @@ namespace SixLabors.ImageSharp.Tests
private static readonly Lazy<string> SolutionDirectoryFullPathLazy = new Lazy<string>(GetSolutionDirectoryFullPathImpl);
private static readonly Lazy<string> NetCoreVersionLazy = new Lazy<string>(GetNetCoreVersion);
private static readonly Lazy<Version> NetCoreVersionLazy = new Lazy<Version>(GetNetCoreVersion);
static TestEnvironment() => PrepareRemoteExecutor();
/// <summary>
/// Gets the .NET Core version, if running on .NET Core, otherwise returns an empty string.
/// </summary>
internal static string NetCoreVersion => NetCoreVersionLazy.Value;
internal static Version NetCoreVersion => NetCoreVersionLazy.Value;
// ReSharper disable once InconsistentNaming
@ -118,7 +118,7 @@ namespace SixLabors.ImageSharp.Tests
internal static bool Is64BitProcess => IntPtr.Size == 8;
internal static bool IsFramework => string.IsNullOrEmpty(NetCoreVersion);
internal static bool IsFramework => NetCoreVersion == null;
/// <summary>
/// A dummy operation to enforce the execution of the static constructor.
@ -262,17 +262,17 @@ namespace SixLabors.ImageSharp.Tests
/// Solution borrowed from:
/// https://github.com/dotnet/BenchmarkDotNet/issues/448#issuecomment-308424100
/// </summary>
private static string GetNetCoreVersion()
private static Version GetNetCoreVersion()
{
Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
string[] assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
{
return assemblyPath[netCoreAppIndex + 1];
return Version.Parse(assemblyPath[netCoreAppIndex + 1]);
}
return string.Empty;
return null;
}
}
}

29
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -416,6 +416,27 @@ namespace SixLabors.ImageSharp.Tests
}
}
public static void CompareBuffers<T>(Buffer2D<T> expected, Buffer2D<T> actual)
where T : struct, IEquatable<T>
{
Assert.True(expected.Size() == actual.Size(), "Buffer sizes are not equal!");
for (int y = 0; y < expected.Height; y++)
{
Span<T> expectedRow = expected.GetRowSpan(y);
Span<T> actualRow = actual.GetRowSpan(y);
for (int x = 0; x < expectedRow.Length; x++)
{
T expectedVal = expectedRow[x];
T actualVal = actualRow[x];
Assert.True(
expectedVal.Equals(actualVal),
$"Buffers differ at position ({x},{y})! Expected: {expectedVal} | Actual: {actualVal}");
}
}
}
/// <summary>
/// All pixels in all frames should be exactly equal to 'expectedPixel'.
/// </summary>
@ -663,7 +684,11 @@ namespace SixLabors.ImageSharp.Tests
this TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
var allocator = (ArrayPoolMemoryAllocator)provider.Configuration.MemoryAllocator;
// TODO: Use a test-only allocator for this.
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
var allocator = ArrayPoolMemoryAllocator.CreateDefault();
#pragma warning restore
provider.Configuration.MemoryAllocator = allocator;
return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf<TPixel>());
}
@ -746,6 +771,7 @@ namespace SixLabors.ImageSharp.Tests
internal class AllocatorBufferCapacityConfigurator
{
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
private readonly ArrayPoolMemoryAllocator allocator;
private readonly int pixelSizeInBytes;
@ -754,6 +780,7 @@ namespace SixLabors.ImageSharp.Tests
this.allocator = allocator;
this.pixelSizeInBytes = pixelSizeInBytes;
}
#pragma warning restore CS0618
public void InBytes(int totalBytes) => this.allocator.BufferCapacityInBytes = totalBytes;

8
tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs

@ -40,12 +40,6 @@ namespace SixLabors.ImageSharp.Tests.Memory
return new BasicArrayBuffer<T>(array, length, this);
}
public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None)
{
byte[] array = this.AllocateArray<byte>(length, options);
return new ManagedByteBuffer(array, this);
}
private T[] AllocateArray<T>(int length, AllocationOptions options)
where T : struct
{
@ -171,7 +165,7 @@ namespace SixLabors.ImageSharp.Tests.Memory
}
}
private class ManagedByteBuffer : BasicArrayBuffer<byte>, IManagedByteBuffer
private class ManagedByteBuffer : BasicArrayBuffer<byte>, IMemoryOwner<byte>
{
public ManagedByteBuffer(byte[] array, TestMemoryAllocator allocator)
: base(array, allocator)

3
tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

@ -165,7 +165,10 @@ namespace SixLabors.ImageSharp.Tests
int width = expected.Width;
expected.Mutate(process);
// TODO: Use a test-only allocator for this
#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete
var allocator = ArrayPoolMemoryAllocator.CreateDefault();
#pragma warning restore CS0618
provider.Configuration.MemoryAllocator = allocator;
allocator.BufferCapacityInBytes = bufferCapacityInPixelRows * width * Unsafe.SizeOf<TPixel>();

Loading…
Cancel
Save