Browse Source

Reimplement buffer ownership management

pull/1730/head
Anton Firszov 5 years ago
parent
commit
77e7700857
  1. 14
      src/ImageSharp/Common/Helpers/DebugGuard.cs
  2. 21
      src/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs
  3. 56
      src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs
  4. 55
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  5. 72
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.Buffer{T}.cs
  6. 50
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs
  7. 201
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs
  8. 27
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs
  9. 60
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
  10. 106
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
  11. 25
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  12. 30
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  13. 2
      src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
  14. 154
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  15. 2
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  16. 11
      tests/ImageSharp.Tests/ConfigurationTests.cs
  17. 1
      tests/ImageSharp.Tests/Image/ImageCloneTests.cs
  18. 26
      tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
  19. 116
      tests/ImageSharp.Tests/Memory/Allocators/RefCountingLifetimeGuardTests.cs
  20. 60
      tests/ImageSharp.Tests/Memory/Allocators/SharedArrayPoolBufferTests.cs
  21. 59
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs
  22. 154
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs
  23. 5
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  24. 93
      tests/ImageSharp.Tests/Memory/Allocators/UnmanagedBufferTests.cs
  25. 142
      tests/ImageSharp.Tests/Memory/Allocators/UnmanagedMemoryHandleTests.cs
  26. 8
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
  27. 2
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs

14
src/ImageSharp/Common/Helpers/DebugGuard.cs

@ -26,6 +26,20 @@ namespace SixLabors
}
}
/// <summary>
/// Verifies whether a specific condition is met, throwing an exception if it's false.
/// </summary>
/// <param name="isDisposed">Whether the object is disposed.</param>
/// <param name="objectName">The name of the object.</param>
[Conditional("DEBUG")]
public static void NotDisposed(bool isDisposed, string objectName)
{
if (isDisposed)
{
throw new ObjectDisposedException(objectName);
}
}
/// <summary>
/// Verifies, that the target span is of same size than the 'other' span.
/// </summary>

21
src/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Defines an common interface for ref-counted objects.
/// </summary>
internal interface IRefCounted
{
/// <summary>
/// Increments the reference counter.
/// </summary>
void AddRef();
/// <summary>
/// Decrements the reference counter.
/// </summary>
void ReleaseRef();
}
}

56
src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs

@ -0,0 +1,56 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Implements reference counting lifetime guard mechanism similar to the one provided by <see cref="SafeHandle"/>,
/// but without the restriction of the guarded object being a handle.
/// </summary>
internal abstract class RefCountedLifetimeGuard : IDisposable
{
private int refCount = 1;
private int disposed;
private int released;
~RefCountedLifetimeGuard()
{
Interlocked.Exchange(ref this.disposed, 1);
this.ReleaseRef();
}
public bool IsDisposed => this.disposed == 1;
public void AddRef() => Interlocked.Increment(ref this.refCount);
public void ReleaseRef()
{
Interlocked.Decrement(ref this.refCount);
if (this.refCount == 0)
{
int wasReleased = Interlocked.Exchange(ref this.released, 1);
if (wasReleased == 0)
{
this.Release();
}
}
}
public void Dispose()
{
int wasDisposed = Interlocked.Exchange(ref this.disposed, 1);
if (wasDisposed == 0)
{
this.ReleaseRef();
GC.SuppressFinalize(this);
}
}
protected abstract void Release();
}
}

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

@ -3,30 +3,26 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>
internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>, IRefCounted
where T : struct
{
private readonly int lengthInBytes;
private byte[] array;
private LifetimeGuard lifetimeGuard;
public SharedArrayPoolBuffer(int lengthInElements)
{
this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>();
this.array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes);
this.lifetimeGuard = new LifetimeGuard(this.array);
}
// The worst thing that could happen is that a VERY poorly written user code holding a Span<TPixel> on the stack,
// while loosing the reference to Image<TPixel> (or disposing it) may write to an unrelated ArrayPool array.
// This is an unlikely scenario we mitigate by a warning in DangerousGetRowSpan(i) APIs.
#pragma warning disable CA2015 // Adding a finalizer to a type derived from MemoryManager<T> may permit memory to be freed while it is still in use by a Span<T>
~SharedArrayPoolBuffer() => this.Dispose(false);
#pragma warning restore
protected override void Dispose(bool disposing)
{
if (this.array == null)
@ -34,12 +30,51 @@ namespace SixLabors.ImageSharp.Memory.Internals
return;
}
ArrayPool<byte>.Shared.Return(this.array);
this.lifetimeGuard.Dispose();
this.array = null;
}
public override Span<T> GetSpan() => MemoryMarshal.Cast<byte, T>(this.array.AsSpan(0, this.lengthInBytes));
public override Span<T> GetSpan()
{
this.CheckDisposed();
return MemoryMarshal.Cast<byte, T>(this.array.AsSpan(0, this.lengthInBytes));
}
protected override object GetPinnableObject() => this.array;
public void AddRef()
{
this.CheckDisposed();
this.lifetimeGuard.AddRef();
}
public void ReleaseRef() => this.lifetimeGuard.ReleaseRef();
[Conditional("DEBUG")]
private void CheckDisposed()
{
if (this.array == null)
{
throw new ObjectDisposedException("SharedArrayPoolBuffer");
}
}
private class LifetimeGuard : RefCountedLifetimeGuard
{
private byte[] array;
public LifetimeGuard(byte[] array) => this.array = array;
protected override void Release()
{
// If this is called by a finalizer, we will end storing the first array of this bucket
// on the thread local storage of the finalizer thread.
// This is not ideal, but subsequent leaks will end up returning arrays to per-cpu buckets,
// meaning likely a different bucket than it was rented from,
// but this is PROBABLY better than not returning the arrays at all.
ArrayPool<byte>.Shared.Return(this.array);
this.array = null;
}
}
}
}

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

@ -1,72 +0,0 @@
// 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 class Buffer<T> : UnmanagedBuffer<T>
where T : struct
{
private UniformUnmanagedMemoryPool pool;
public Buffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length)
: base(bufferHandle, length) =>
this.pool = pool;
/// <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 sealed class FinalizableBuffer<T> : Buffer<T>
where T : struct
{
public FinalizableBuffer(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle bufferHandle, int length)
: base(pool, bufferHandle, length)
{
bufferHandle.AssignedToNewOwner();
}
// A VERY poorly written user code holding a Span<TPixel> on the stack,
// while loosing the reference to Image<TPixel> (or disposing it) may write to (now unrelated) pool buffer,
// or cause memory corruption if the underlying UmnanagedMemoryHandle has been released.
// This is an unlikely scenario we mitigate a warning in DangerousGetRowSpan(i) APIs.
#pragma warning disable CA2015 // Adding a finalizer to a type derived from MemoryManager<T> may permit memory to be freed while it is still in use by a Span<T>
~FinalizableBuffer() => this.Dispose(false);
#pragma warning restore
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);
GC.SuppressFinalize(this);
}
}
}
}

50
src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory.Internals
{
internal partial class UniformUnmanagedMemoryPool
{
public UnmanagedBuffer<T> CreateGuardedBuffer<T>(
UnmanagedMemoryHandle handle,
int lengthInElements,
AllocationOptions options)
where T : struct
{
var buffer = new UnmanagedBuffer<T>(lengthInElements, new ReturnToPoolBufferLifetimeGuard(this, handle));
if (options.Has(AllocationOptions.Clean))
{
buffer.Clear();
}
return buffer;
}
public RefCountedLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles);
private sealed class GroupLifetimeGuard : RefCountedLifetimeGuard
{
private readonly UniformUnmanagedMemoryPool pool;
private readonly UnmanagedMemoryHandle[] handles;
public GroupLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] handles)
{
this.pool = pool;
this.handles = handles;
}
protected override void Release() => this.pool.Return(this.handles);
}
private sealed class ReturnToPoolBufferLifetimeGuard : UnmanagedBufferLifetimeGuard
{
private readonly UniformUnmanagedMemoryPool pool;
public ReturnToPoolBufferLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle handle)
: base(handle) =>
this.pool = pool;
protected override void Release() => this.pool.Return(this.Handle);
}
}
}

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

@ -2,19 +2,24 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal partial class UniformUnmanagedMemoryPool
{
private static int minTrimPeriodMilliseconds = int.MaxValue;
private static readonly List<WeakReference<UniformUnmanagedMemoryPool>> AllPools = new();
private static Timer trimTimer;
private static readonly Stopwatch Stopwatch = Stopwatch.StartNew();
private readonly TrimSettings trimSettings;
private UnmanagedMemoryHandle[] buffers;
private readonly UnmanagedMemoryHandle[] buffers;
private int index;
private Timer trimTimer;
private long lastTrimTimestamp;
public UniformUnmanagedMemoryPool(int bufferLength, int capacity)
@ -31,16 +36,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
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.
var weakPoolRef = new WeakReference<UniformUnmanagedMemoryPool>(this);
this.trimTimer = new Timer(
s => TimerCallback((WeakReference<UniformUnmanagedMemoryPool>)s),
weakPoolRef,
this.trimSettings.TrimPeriodMilliseconds / 4,
this.trimSettings.TrimPeriodMilliseconds / 4);
UpdateTimer(trimSettings, this);
#if NETCORE31COMPATIBLE
Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this);
#endif
@ -52,31 +48,33 @@ namespace SixLabors.ImageSharp.Memory.Internals
public int Capacity { get; }
/// <summary>
/// Rent a single buffer or return <see cref="UnmanagedMemoryHandle.NullHandle"/> if the pool is full.
/// </summary>
public UnmanagedMemoryHandle Rent()
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
// Avoid taking the lock if the pool is released or is over limit:
if (buffersLocal == null || this.index == buffersLocal.Length)
// Avoid taking the lock if the pool is is over it's limit:
if (this.index == buffersLocal.Length)
{
return null;
return UnmanagedMemoryHandle.NullHandle;
}
UnmanagedMemoryHandle buffer;
lock (buffersLocal)
{
// Check again after taking the lock:
if (this.buffers == null || this.index == buffersLocal.Length)
if (this.index == buffersLocal.Length)
{
return null;
return UnmanagedMemoryHandle.NullHandle;
}
buffer = buffersLocal[this.index];
buffersLocal[this.index++] = null;
buffersLocal[this.index++] = default;
}
if (buffer == null)
if (buffer.IsInvalid)
{
buffer = UnmanagedMemoryHandle.Allocate(this.BufferLength);
}
@ -84,12 +82,15 @@ namespace SixLabors.ImageSharp.Memory.Internals
return buffer;
}
public UnmanagedMemoryHandle[] Rent(int bufferCount, AllocationOptions allocationOptions = AllocationOptions.None)
/// <summary>
/// Rent <paramref name="bufferCount"/> buffers or return 'null' if the pool is full.
/// </summary>
public UnmanagedMemoryHandle[] Rent(int bufferCount)
{
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)
// Avoid taking the lock if the pool is is over it's limit:
if (this.index + bufferCount >= buffersLocal.Length + 1)
{
return null;
}
@ -98,7 +99,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
lock (buffersLocal)
{
// Check again after taking the lock:
if (this.buffers == null || this.index + bufferCount >= buffersLocal.Length + 1)
if (this.index + bufferCount >= buffersLocal.Length + 1)
{
return null;
}
@ -107,128 +108,138 @@ namespace SixLabors.ImageSharp.Memory.Internals
for (int i = 0; i < bufferCount; i++)
{
result[i] = buffersLocal[this.index];
buffersLocal[this.index++] = null;
buffersLocal[this.index++] = UnmanagedMemoryHandle.NullHandle;
}
}
for (int i = 0; i < result.Length; i++)
{
if (result[i] == null)
if (result[i].IsInvalid)
{
result[i] = UnmanagedMemoryHandle.Allocate(this.BufferLength);
}
if (allocationOptions.Has(AllocationOptions.Clean))
{
this.GetSpan(result[i]).Clear();
}
}
return result;
}
public void Return(UnmanagedMemoryHandle buffer)
public void Return(UnmanagedMemoryHandle bufferHandle)
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
if (buffersLocal == null)
{
buffer.Dispose();
return;
}
lock (buffersLocal)
Guard.IsTrue(bufferHandle.IsValid, nameof(bufferHandle), "Returning NullHandle to the pool is not allowed.");
lock (this.buffers)
{
// Check again after taking the lock:
if (this.buffers == null)
{
buffer.Dispose();
return;
}
if (this.index == 0)
{
ThrowReturnedMoreBuffersThanRented(); // DEBUG-only exception
buffer.Dispose();
bufferHandle.Free();
return;
}
this.buffers[--this.index] = buffer;
this.buffers[--this.index] = bufferHandle;
}
}
public void Return(Span<UnmanagedMemoryHandle> buffers)
public void Return(Span<UnmanagedMemoryHandle> bufferHandles)
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
if (buffersLocal == null)
{
DisposeAll(buffers);
return;
}
lock (buffersLocal)
lock (this.buffers)
{
// Check again after taking the lock:
if (this.buffers == null)
{
DisposeAll(buffers);
return;
}
if (this.index - buffers.Length + 1 <= 0)
if (this.index - bufferHandles.Length + 1 <= 0)
{
ThrowReturnedMoreBuffersThanRented();
DisposeAll(buffers);
DisposeAll(bufferHandles);
return;
}
for (int i = buffers.Length - 1; i >= 0; i--)
for (int i = bufferHandles.Length - 1; i >= 0; i--)
{
buffersLocal[--this.index] = buffers[i];
ref UnmanagedMemoryHandle h = ref bufferHandles[i];
Guard.IsTrue(h.IsValid, nameof(bufferHandles), "Returning NullHandle to the pool is not allowed.");
this.buffers[--this.index] = bufferHandles[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);
lock (this.buffers)
{
for (int i = this.index; i < this.buffers.Length; i++)
{
UnmanagedMemoryHandle buffer = this.buffers[i];
if (buffer.IsInvalid)
{
break;
}
buffer.Free();
this.buffers[i] = UnmanagedMemoryHandle.NullHandle;
}
}
}
private static void DisposeAll(Span<UnmanagedMemoryHandle> buffers)
{
foreach (UnmanagedMemoryHandle handle in buffers)
{
handle?.Dispose();
handle.Free();
}
}
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 ThrowReturnedMoreBuffersThanRented() =>
throw new InvalidMemoryOperationException("Returned more buffers then rented");
private static void TimerCallback(WeakReference<UniformUnmanagedMemoryPool> weakPoolRef)
private static void UpdateTimer(TrimSettings settings, UniformUnmanagedMemoryPool pool)
{
if (weakPoolRef.TryGetTarget(out UniformUnmanagedMemoryPool pool))
lock (AllPools)
{
pool.Trim();
AllPools.Add(new WeakReference<UniformUnmanagedMemoryPool>(pool));
// Invoke the timer callback more frequently, than trimSettings.TrimPeriodMilliseconds.
// We are checking in the callback if enough time passed since the last trimming. If not, we do nothing.
int period = settings.TrimPeriodMilliseconds / 4;
if (trimTimer == null)
{
trimTimer = new Timer(_ => TimerCallback(), null, period, period);
}
else if (settings.TrimPeriodMilliseconds < minTrimPeriodMilliseconds)
{
trimTimer.Change(period, period);
}
minTrimPeriodMilliseconds = Math.Min(minTrimPeriodMilliseconds, settings.TrimPeriodMilliseconds);
}
}
private bool Trim()
private static void TimerCallback()
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
if (buffersLocal == null)
lock (AllPools)
{
return false;
// Remove lost references from the list:
for (int i = AllPools.Count - 1; i >= 0; i--)
{
if (!AllPools[i].TryGetTarget(out _))
{
AllPools.RemoveAt(i);
}
}
foreach (WeakReference<UniformUnmanagedMemoryPool> weakPoolRef in AllPools)
{
if (weakPoolRef.TryGetTarget(out UniformUnmanagedMemoryPool pool))
{
pool.Trim();
}
}
}
}
private bool Trim()
{
UnmanagedMemoryHandle[] buffersLocal = this.buffers;
bool isHighPressure = this.IsHighMemoryPressure();
@ -250,16 +261,11 @@ namespace SixLabors.ImageSharp.Memory.Internals
{
lock (buffersLocal)
{
if (this.buffers == null)
{
return false;
}
// Trim all:
for (int i = this.index; i < buffersLocal.Length && buffersLocal[i] != null; i++)
for (int i = this.index; i < buffersLocal.Length && buffersLocal[i].IsValid; i++)
{
buffersLocal[i].Dispose();
buffersLocal[i] = null;
buffersLocal[i].Free();
buffersLocal[i] = UnmanagedMemoryHandle.NullHandle;
}
}
@ -270,14 +276,9 @@ namespace SixLabors.ImageSharp.Memory.Internals
{
lock (buffersLocal)
{
if (this.buffers == null)
{
return false;
}
// Count the buffers in the pool:
int retainedCount = 0;
for (int i = this.index; i < buffersLocal.Length && buffersLocal[i] != null; i++)
for (int i = this.index; i < buffersLocal.Length && buffersLocal[i].IsValid; i++)
{
retainedCount++;
}
@ -288,8 +289,8 @@ namespace SixLabors.ImageSharp.Memory.Internals
int trimStop = this.index + retainedCount - trimCount;
for (int i = trimStart; i >= trimStop; i--)
{
buffersLocal[i].Dispose();
buffersLocal[i] = null;
buffersLocal[i].Free();
buffersLocal[i] = UnmanagedMemoryHandle.NullHandle;
}
this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds;

27
src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs

@ -0,0 +1,27 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory.Internals
{
/// <summary>
/// Defines a strategy for managing unmanaged memory ownership.
/// </summary>
internal abstract class UnmanagedBufferLifetimeGuard : RefCountedLifetimeGuard
{
private UnmanagedMemoryHandle handle;
protected UnmanagedBufferLifetimeGuard(UnmanagedMemoryHandle handle) => this.handle = handle;
public UnmanagedMemoryHandle Handle => this.handle;
public sealed class FreeHandle : UnmanagedBufferLifetimeGuard
{
public FreeHandle(UnmanagedMemoryHandle handle)
: base(handle)
{
}
protected override void Release() => this.handle.Free();
}
}
}

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

@ -5,6 +5,7 @@ using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals
{
@ -13,58 +14,67 @@ namespace SixLabors.ImageSharp.Memory.Internals
/// access to unmanaged buffers allocated by <see cref="Marshal.AllocHGlobal(int)"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
internal unsafe class UnmanagedBuffer<T> : MemoryManager<T>
internal sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T>, IRefCounted
where T : struct
{
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(UnmanagedMemoryHandle.Allocate(lengthInElements * Unsafe.SizeOf<T>()), lengthInElements)
{
}
private readonly UnmanagedBufferLifetimeGuard lifetimeGuard;
private int disposed;
protected UnmanagedBuffer(UnmanagedMemoryHandle bufferHandle, int lengthInElements)
public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifetimeGuard)
{
DebugGuard.NotNull(lifetimeGuard, nameof(lifetimeGuard));
this.lengthInElements = lengthInElements;
this.BufferHandle = bufferHandle;
this.lifetimeGuard = lifetimeGuard;
}
public UnmanagedMemoryHandle BufferHandle { get; protected set; }
private void* Pointer => (void*)this.BufferHandle.DangerousGetHandle();
private void* Pointer => this.lifetimeGuard.Handle.Pointer;
public override Span<T> GetSpan() => new(this.Pointer, this.lengthInElements);
public override Span<T> GetSpan()
{
DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name);
DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name);
return new(this.Pointer, this.lengthInElements);
}
/// <inheritdoc />
public override MemoryHandle Pin(int elementIndex = 0)
{
DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name);
DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name);
// Will be released in Unpin
bool unused = false;
this.BufferHandle.DangerousAddRef(ref unused);
this.lifetimeGuard.AddRef();
void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex);
return new MemoryHandle(pbData, pinnable: this);
}
/// <inheritdoc />
public override void Unpin() => this.BufferHandle.DangerousRelease();
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (this.BufferHandle.IsInvalid)
DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!");
if (Interlocked.Exchange(ref this.disposed, 1) == 1)
{
// Already disposed
return;
}
if (disposing)
{
this.BufferHandle.Dispose();
}
this.lifetimeGuard.Dispose();
}
/// <inheritdoc />
public override void Unpin() => this.lifetimeGuard.ReleaseRef();
public void AddRef() => this.lifetimeGuard.AddRef();
public void ReleaseRef() => this.lifetimeGuard.ReleaseRef();
public static UnmanagedBuffer<T> Allocate(int lengthInElements) =>
new(lengthInElements, new UnmanagedBufferLifetimeGuard.FreeHandle(UnmanagedMemoryHandle.Allocate(lengthInElements * Unsafe.SizeOf<T>())));
}
}

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

@ -2,20 +2,20 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal sealed class UnmanagedMemoryHandle : SafeHandle
/// <summary>
/// Encapsulates the functionality around allocating and releasing unmanaged memory. NOT a <see cref="SafeHandle"/>.
/// </summary>
internal struct UnmanagedMemoryHandle : IEquatable<UnmanagedMemoryHandle>
{
// Number of allocation re-attempts when OutOfMemoryException is thrown.
// Number of allocation re-attempts when detecting OutOfMemoryException.
private const int MaxAllocationAttempts = 1000;
private readonly int lengthInBytes;
private bool resurrected;
// Track allocations for testing purposes:
private static int totalOutstandingHandles;
@ -24,10 +24,16 @@ namespace SixLabors.ImageSharp.Memory.Internals
// A Monitor to wait/signal when we are low on memory.
private static object lowMemoryMonitor;
public static readonly UnmanagedMemoryHandle NullHandle = default;
private IntPtr handle;
private readonly int lengthInBytes;
private UnmanagedMemoryHandle(IntPtr handle, int lengthInBytes)
: base(handle, true)
{
this.handle = handle;
this.lengthInBytes = lengthInBytes;
if (lengthInBytes > 0)
{
GC.AddMemoryPressure(lengthInBytes);
@ -36,6 +42,14 @@ namespace SixLabors.ImageSharp.Memory.Internals
Interlocked.Increment(ref totalOutstandingHandles);
}
public IntPtr Handle => this.handle;
public bool IsInvalid => this.Handle == IntPtr.Zero;
public bool IsValid => this.Handle != IntPtr.Zero;
public unsafe void* Pointer => (void*)this.Handle;
/// <summary>
/// Gets the total outstanding handle allocations for testing purposes.
/// </summary>
@ -46,36 +60,34 @@ namespace SixLabors.ImageSharp.Memory.Internals
/// </summary>
internal static long TotalOomRetries => totalOomRetries;
/// <inheritdoc />
public override bool IsInvalid => this.handle == IntPtr.Zero;
public static bool operator ==(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => a.Equals(b);
public static bool operator !=(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => !a.Equals(b);
protected override bool ReleaseHandle()
[MethodImpl(InliningOptions.HotPath)]
public unsafe Span<byte> GetSpan()
{
if (this.IsInvalid)
{
return false;
ThrowDisposed();
}
Marshal.FreeHGlobal(this.handle);
if (this.lengthInBytes > 0)
{
GC.RemoveMemoryPressure(this.lengthInBytes);
}
return new Span<byte>(this.Pointer, this.lengthInBytes);
}
if (lowMemoryMonitor != null)
[MethodImpl(InliningOptions.HotPath)]
public unsafe Span<byte> GetSpan(int lengthInBytes)
{
DebugGuard.MustBeLessThanOrEqualTo(lengthInBytes, this.lengthInBytes, nameof(lengthInBytes));
if (this.IsInvalid)
{
// We are low on memory. Signal all threads waiting in AllocateHandle().
Monitor.Enter(lowMemoryMonitor);
Monitor.PulseAll(lowMemoryMonitor);
Monitor.Exit(lowMemoryMonitor);
ThrowDisposed();
}
this.handle = IntPtr.Zero;
Interlocked.Decrement(ref totalOutstandingHandles);
return true;
return new Span<byte>(this.Pointer, lengthInBytes);
}
internal static UnmanagedMemoryHandle Allocate(int lengthInBytes)
public static UnmanagedMemoryHandle Allocate(int lengthInBytes)
{
IntPtr handle = AllocateHandle(lengthInBytes);
return new UnmanagedMemoryHandle(handle, lengthInBytes);
@ -115,26 +127,38 @@ namespace SixLabors.ImageSharp.Memory.Internals
return handle;
}
/// <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()
public void Free()
{
GC.SuppressFinalize(this);
this.resurrected = true;
}
IntPtr h = Interlocked.Exchange(ref this.handle, IntPtr.Zero);
internal void AssignedToNewOwner()
{
if (this.resurrected)
if (h == IntPtr.Zero)
{
return;
}
Marshal.FreeHGlobal(h);
Interlocked.Decrement(ref totalOutstandingHandles);
if (this.lengthInBytes > 0)
{
GC.RemoveMemoryPressure(this.lengthInBytes);
}
if (Volatile.Read(ref lowMemoryMonitor) != null)
{
// The handle has been resurrected
GC.ReRegisterForFinalize(this);
this.resurrected = false;
// We are low on memory. Signal all threads waiting in AllocateHandle().
Monitor.Enter(lowMemoryMonitor);
Monitor.PulseAll(lowMemoryMonitor);
Monitor.Exit(lowMemoryMonitor);
}
}
public bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
public override bool Equals(object obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
public override int GetHashCode() => this.handle.GetHashCode();
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(UnmanagedMemoryHandle));
}
}

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

@ -11,26 +11,15 @@ namespace SixLabors.ImageSharp.Memory
/// </summary>
public abstract class MemoryAllocator
{
private static MemoryAllocator defaultMemoryAllocator = Create();
/// <summary>
/// Gets or sets the default global <see cref="MemoryAllocator"/> instance for the current process.
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
/// serves as the default value for <see cref="Configuration.MemoryAllocator"/>.
/// <para />
/// This is a get-only property,
/// you should set <see cref="Configuration.Default"/>'s <see cref="Configuration.MemoryAllocator"/>
/// to change the default allocator used by <see cref="Image"/> and it's operations.
/// </summary>
/// <remarks>
/// Since <see cref="Configuration.Default"/> is lazy-initialized, setting the value of <see cref="Default"/>
/// will only override <see cref="Configuration.Default"/>'s <see cref="Configuration.MemoryAllocator"/>
/// before the first read of the <see cref="Configuration.Default"/> property.
/// After that, a manual assigment of <see cref="Configuration.Default"/> is necessary.
/// </remarks>
public static MemoryAllocator Default
{
get => defaultMemoryAllocator;
set
{
Guard.NotNull(value, nameof(Default));
defaultMemoryAllocator = value;
}
}
public static MemoryAllocator Default { get; } = Create();
/// <summary>
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.

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

@ -98,15 +98,10 @@ namespace SixLabors.ImageSharp.Memory
if (lengthInBytes <= this.poolBufferSizeInBytes)
{
UnmanagedMemoryHandle array = this.pool.Rent();
if (array != null)
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
var buffer = new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, length);
if (options.Has(AllocationOptions.Clean))
{
buffer.Clear();
}
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options);
return buffer;
}
}
@ -130,15 +125,10 @@ namespace SixLabors.ImageSharp.Memory
if (totalLengthInBytes <= this.poolBufferSizeInBytes)
{
// Optimized path renting single array from the pool
UnmanagedMemoryHandle array = this.pool.Rent();
if (array != null)
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
var buffer = new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, (int)totalLength);
if (options.Has(AllocationOptions.Clean))
{
buffer.Clear();
}
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, (int)totalLength, options);
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}
}
@ -152,13 +142,7 @@ namespace SixLabors.ImageSharp.Memory
return 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, this.trimSettings));
oldPool.Release();
}
public override void ReleaseRetainedResources() => this.pool.Release();
private static long GetDefaultMaxPoolSizeBytes()
{

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

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Memory
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
{
var buffer = new UnmanagedBuffer<T>(length);
var buffer = UnmanagedBuffer<T>.Allocate(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();

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

@ -18,9 +18,7 @@ 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;
private RefCountedLifetimeGuard groupLifetimeGuard;
public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable)
: base(bufferLength, totalLength)
@ -30,14 +28,15 @@ namespace SixLabors.ImageSharp.Memory
this.View = new MemoryGroupView<T>(this);
}
public Owned(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] pooledArrays, int bufferLength, long totalLength, int sizeOfLastBuffer, AllocationOptions options)
: this(CreateBuffers(pool, pooledArrays, bufferLength, sizeOfLastBuffer, options), bufferLength, totalLength, true)
{
this.pooledHandles = pooledArrays;
this.unmanagedMemoryPool = pool;
}
~Owned() => this.Dispose(false);
public Owned(
UniformUnmanagedMemoryPool pool,
UnmanagedMemoryHandle[] pooledHandles,
int bufferLength,
long totalLength,
int sizeOfLastBuffer,
AllocationOptions options)
: this(CreateBuffers(pooledHandles, bufferLength, sizeOfLastBuffer, options), bufferLength, totalLength, true) =>
this.groupLifetimeGuard = pool.CreateGroupLifetimeGuard(pooledHandles);
public bool Swappable { get; }
@ -63,7 +62,6 @@ namespace SixLabors.ImageSharp.Memory
}
private static IMemoryOwner<T>[] CreateBuffers(
UniformUnmanagedMemoryPool pool,
UnmanagedMemoryHandle[] pooledBuffers,
int bufferLength,
int sizeOfLastBuffer,
@ -72,42 +70,35 @@ namespace SixLabors.ImageSharp.Memory
var result = new IMemoryOwner<T>[pooledBuffers.Length];
for (int i = 0; i < pooledBuffers.Length - 1; i++)
{
pooledBuffers[i].AssignedToNewOwner();
var currentBuffer = new UniformUnmanagedMemoryPool.Buffer<T>(pool, pooledBuffers[i], bufferLength);
if (options.Has(AllocationOptions.Clean))
{
currentBuffer.Clear();
}
var currentBuffer = ObservedBuffer.Create(pooledBuffers[i], bufferLength, options);
result[i] = currentBuffer;
}
var lastBuffer = new UniformUnmanagedMemoryPool.Buffer<T>(pool, pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer);
if (options.Has(AllocationOptions.Clean))
{
lastBuffer.Clear();
}
var lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options);
result[result.Length - 1] = lastBuffer;
return result;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}
public override MemoryGroupEnumerator<T> GetEnumerator() => new(this);
public override void IncreaseRefCounts()
{
this.EnsureNotDisposed();
bool dummy = default;
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
if (this.groupLifetimeGuard != null)
{
this.groupLifetimeGuard.AddRef();
}
else
{
if (memoryOwner is UnmanagedBuffer<T> unmanagedBuffer)
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
{
unmanagedBuffer.BufferHandle?.DangerousAddRef(ref dummy);
if (memoryOwner is IRefCounted unmanagedBuffer)
{
unmanagedBuffer.AddRef();
}
}
}
}
@ -115,11 +106,18 @@ namespace SixLabors.ImageSharp.Memory
public override void DecreaseRefCounts()
{
this.EnsureNotDisposed();
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
if (this.groupLifetimeGuard != null)
{
if (memoryOwner is UnmanagedBuffer<T> unmanagedBuffer)
this.groupLifetimeGuard.ReleaseRef();
}
else
{
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
{
unmanagedBuffer.BufferHandle?.DangerousRelease();
if (memoryOwner is IRefCounted unmanagedBuffer)
{
unmanagedBuffer.ReleaseRef();
}
}
}
}
@ -133,34 +131,18 @@ namespace SixLabors.ImageSharp.Memory
protected override void Dispose(bool disposing)
{
if (this.IsDisposed)
if (this.IsDisposed || !disposing)
{
return;
}
this.View.Invalidate();
if (this.unmanagedMemoryPool != null)
if (this.groupLifetimeGuard != null)
{
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();
}
GC.SuppressFinalize(this);
this.groupLifetimeGuard.Dispose();
}
else if (disposing)
else
{
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
{
@ -170,9 +152,7 @@ namespace SixLabors.ImageSharp.Memory
this.memoryOwners = null;
this.IsValid = false;
this.pooledArrays = null;
this.unmanagedMemoryPool = null;
this.pooledHandles = null;
this.groupLifetimeGuard = null;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -195,29 +175,67 @@ 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;
RefCountedLifetimeGuard tempGroupOwner = a.groupLifetimeGuard;
a.memoryOwners = b.memoryOwners;
a.TotalLength = b.TotalLength;
a.BufferLength = b.BufferLength;
a.pooledArrays = b.pooledArrays;
a.unmanagedMemoryPool = b.unmanagedMemoryPool;
a.pooledHandles = b.pooledHandles;
a.groupLifetimeGuard = b.groupLifetimeGuard;
b.memoryOwners = tempOwners;
b.TotalLength = tempTotalLength;
b.BufferLength = tempBufferLength;
b.pooledArrays = tempPooledArrays;
b.unmanagedMemoryPool = tempUnmangedPool;
b.pooledHandles = tempPooledHandles;
b.groupLifetimeGuard = tempGroupOwner;
a.View.Invalidate();
b.View.Invalidate();
a.View = new MemoryGroupView<T>(a);
b.View = new MemoryGroupView<T>(b);
}
// No-ownership
private sealed class ObservedBuffer : MemoryManager<T>
{
private readonly UnmanagedMemoryHandle handle;
private readonly int lengthInElements;
private ObservedBuffer(UnmanagedMemoryHandle handle, int lengthInElements)
{
this.handle = handle;
this.lengthInElements = lengthInElements;
}
public static ObservedBuffer Create(
UnmanagedMemoryHandle handle,
int lengthInElements,
AllocationOptions options)
{
var buffer = new ObservedBuffer(handle, lengthInElements);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return buffer;
}
protected override void Dispose(bool disposing)
{
// No-op.
}
public override unsafe Span<T> GetSpan() => new(this.handle.Pointer, this.lengthInElements);
public override unsafe MemoryHandle Pin(int elementIndex = 0)
{
void* pbData = Unsafe.Add<T>(this.handle.Pointer, elementIndex);
return new MemoryHandle(pbData);
}
public override void Unpin()
{
}
}
}
}
}

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

@ -192,7 +192,7 @@ namespace SixLabors.ImageSharp.Memory
bufferCount++;
}
UnmanagedMemoryHandle[] arrays = pool.Rent(bufferCount, options);
UnmanagedMemoryHandle[] arrays = pool.Rent(bufferCount);
if (arrays == null)
{

11
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -156,17 +156,14 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest()
{
MemoryAllocator allocator = new TestMemoryAllocator();
MemoryAllocator.Default = allocator;
var c1 = new Configuration();
var c2 = new Configuration(new MockConfigurationModule());
var c3 = Configuration.CreateDefaultInstance();
Assert.Same(allocator, Configuration.Default.MemoryAllocator);
Assert.Same(allocator, c1.MemoryAllocator);
Assert.Same(allocator, c2.MemoryAllocator);
Assert.Same(allocator, c3.MemoryAllocator);
Assert.Same(MemoryAllocator.Default, Configuration.Default.MemoryAllocator);
Assert.Same(MemoryAllocator.Default, c1.MemoryAllocator);
Assert.Same(MemoryAllocator.Default, c2.MemoryAllocator);
Assert.Same(MemoryAllocator.Default, c3.MemoryAllocator);
}
}

1
tests/ImageSharp.Tests/Image/ImageCloneTests.cs

@ -134,7 +134,6 @@ namespace SixLabors.ImageSharp.Tests
}
}
});
}
}
}

26
tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs

@ -181,7 +181,7 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string testTypeName, string throwExceptionStr)
{
bool throwExceptionInner = bool.Parse(throwExceptionStr);
var buffer = new UnmanagedBuffer<L8>(100);
var buffer = UnmanagedBuffer<L8>.Allocate(100);
var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer);
Configuration.Default.MemoryAllocator = allocator;
@ -192,7 +192,7 @@ namespace SixLabors.ImageSharp.Tests
{
GetTest(testTypeName).ProcessPixelRowsImpl(image, _ =>
{
buffer.BufferHandle.Dispose();
((IDisposable)buffer).Dispose();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
if (throwExceptionInner)
{
@ -218,8 +218,8 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string testTypeName, string throwExceptionStr)
{
bool throwExceptionInner = bool.Parse(throwExceptionStr);
var buffer1 = new UnmanagedBuffer<L8>(100);
var buffer2 = new UnmanagedBuffer<L8>(100);
var buffer1 = UnmanagedBuffer<L8>.Allocate(100);
var buffer2 = UnmanagedBuffer<L8>.Allocate(100);
var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer1, buffer2);
Configuration.Default.MemoryAllocator = allocator;
@ -231,8 +231,8 @@ namespace SixLabors.ImageSharp.Tests
{
GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, (_, _) =>
{
buffer1.BufferHandle.Dispose();
buffer2.BufferHandle.Dispose();
((IDisposable)buffer1).Dispose();
((IDisposable)buffer2).Dispose();
Assert.Equal(2, UnmanagedMemoryHandle.TotalOutstandingHandles);
if (throwExceptionInner)
{
@ -258,9 +258,9 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string testTypeName, string throwExceptionStr)
{
bool throwExceptionInner = bool.Parse(throwExceptionStr);
var buffer1 = new UnmanagedBuffer<L8>(100);
var buffer2 = new UnmanagedBuffer<L8>(100);
var buffer3 = new UnmanagedBuffer<L8>(100);
var buffer1 = UnmanagedBuffer<L8>.Allocate(100);
var buffer2 = UnmanagedBuffer<L8>.Allocate(100);
var buffer3 = UnmanagedBuffer<L8>.Allocate(100);
var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer1, buffer2, buffer3);
Configuration.Default.MemoryAllocator = allocator;
@ -273,9 +273,9 @@ namespace SixLabors.ImageSharp.Tests
{
GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, image3, (_, _, _) =>
{
buffer1.BufferHandle.Dispose();
buffer2.BufferHandle.Dispose();
buffer3.BufferHandle.Dispose();
((IDisposable)buffer1).Dispose();
((IDisposable)buffer2).Dispose();
((IDisposable)buffer3).Dispose();
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles);
if (throwExceptionInner)
{
@ -317,7 +317,7 @@ namespace SixLabors.ImageSharp.Tests
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) =>
(IMemoryOwner<T>)this.buffers.Pop();
this.buffers.Pop() as IMemoryOwner<T>;
}
}
}

116
tests/ImageSharp.Tests/Memory/Allocators/RefCountingLifetimeGuardTests.cs

@ -0,0 +1,116 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Runtime.CompilerServices;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public class RefCountingLifetimeGuardTests
{
[Theory]
[InlineData(1)]
[InlineData(3)]
public void Dispose_ResultsInSingleRelease(int disposeCount)
{
var guard = new MockLifetimeGuard();
Assert.Equal(0, guard.ReleaseInvocationCount);
for (int i = 0; i < disposeCount; i++)
{
guard.Dispose();
}
Assert.Equal(1, guard.ReleaseInvocationCount);
}
[Fact]
public void Finalize_ResultsInSingleRelease()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
Assert.Equal(0, MockLifetimeGuard.GlobalReleaseInvocationCount);
LeakGuard(false);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(1, MockLifetimeGuard.GlobalReleaseInvocationCount);
}
}
[Theory]
[InlineData(1)]
[InlineData(3)]
public void AddRef_PreventsReleaseOnDispose(int addRefCount)
{
var guard = new MockLifetimeGuard();
for (int i = 0; i < addRefCount; i++)
{
guard.AddRef();
}
guard.Dispose();
for (int i = 0; i < addRefCount; i++)
{
Assert.Equal(0, guard.ReleaseInvocationCount);
guard.ReleaseRef();
}
Assert.Equal(1, guard.ReleaseInvocationCount);
}
[Fact]
public void AddRef_PreventsReleaseOnFinalize()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
LeakGuard(true);
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(0, MockLifetimeGuard.GlobalReleaseInvocationCount);
}
}
[Fact]
public void AddRefReleaseRefMisuse_DoesntLeadToMultipleReleases()
{
var guard = new MockLifetimeGuard();
guard.Dispose();
guard.AddRef();
guard.ReleaseRef();
Assert.Equal(1, guard.ReleaseInvocationCount);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void LeakGuard(bool addRef)
{
var guard = new MockLifetimeGuard();
if (addRef)
{
guard.AddRef();
}
}
private class MockLifetimeGuard : RefCountedLifetimeGuard
{
public int ReleaseInvocationCount { get; private set; }
public static int GlobalReleaseInvocationCount { get; private set; }
protected override void Release()
{
this.ReleaseInvocationCount++;
GlobalReleaseInvocationCount++;
}
}
}
}

60
tests/ImageSharp.Tests/Memory/Allocators/SharedArrayPoolBufferTests.cs

@ -0,0 +1,60 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Linq;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public class SharedArrayPoolBufferTests
{
[Fact]
public void AllocatesArrayPoolArray()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
using (var buffer = new SharedArrayPoolBuffer<byte>(900))
{
Assert.Equal(900, buffer.GetSpan().Length);
buffer.GetSpan().Fill(42);
}
byte[] array = ArrayPool<byte>.Shared.Rent(900);
byte[] expected = Enumerable.Repeat((byte)42, 900).ToArray();
Assert.True(expected.AsSpan().SequenceEqual(array.AsSpan(0, 900)));
}
}
[Fact]
public void OutstandingReferences_RetainArrays()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var buffer = new SharedArrayPoolBuffer<byte>(900);
Span<byte> span = buffer.GetSpan();
buffer.AddRef();
((IDisposable)buffer).Dispose();
span.Fill(42);
byte[] array = ArrayPool<byte>.Shared.Rent(900);
Assert.NotEqual(42, array[0]);
ArrayPool<byte>.Shared.Return(array);
buffer.ReleaseRef();
array = ArrayPool<byte>.Shared.Rent(900);
byte[] expected = Enumerable.Repeat((byte)42, 900).ToArray();
Assert.True(expected.AsSpan().SequenceEqual(array.AsSpan(0, 900)));
ArrayPool<byte>.Shared.Return(array);
}
}
}
}

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

@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals;
@ -13,14 +15,14 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public partial class UniformUnmanagedMemoryPoolTests
{
[CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)]
public class NonParallelTests
{
}
[Collection(nameof(NonParallelTests))]
public class Trim
{
[CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)]
public class NonParallelTests
{
}
[Fact]
public void TrimPeriodElapsed_TrimsHalfOfUnusedArrays()
{
@ -45,18 +47,56 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
}
}
[Fact]
public void MultiplePoolInstances_TrimPeriodElapsed_AllAreTrimmed()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var trimSettings1 = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 6_000 };
var pool1 = new UniformUnmanagedMemoryPool(128, 256, trimSettings1);
Thread.Sleep(8_000); // Let some callbacks fire already
var trimSettings2 = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 3_000 };
var pool2 = new UniformUnmanagedMemoryPool(128, 256, trimSettings2);
pool1.Return(pool1.Rent(64));
pool2.Return(pool2.Rent(64));
Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles);
// This exercises pool weak reference list trimming:
LeakPoolInstance();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles);
Thread.Sleep(15_000);
Assert.True(
UnmanagedMemoryHandle.TotalOutstandingHandles <= 64,
$"UnmanagedMemoryHandle.TotalOutstandingHandles={UnmanagedMemoryHandle.TotalOutstandingHandles} > 80");
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void LeakPoolInstance()
{
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 4_000 };
_ = new UniformUnmanagedMemoryPool(128, 256, trimSettings);
}
}
#if NETCORE31COMPATIBLE
public static readonly bool Is32BitProcess = !Environment.Is64BitProcess;
private static readonly List<byte[]> PressureArrays = new List<byte[]>();
private static readonly List<byte[]> PressureArrays = new();
[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;
const int OneMb = 1 << 20;
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { HighPressureThresholdRate = 0.2f };
@ -82,6 +122,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
// Prevent eager collection of the pool:
GC.KeepAlive(pool);
static void TouchPage(byte[] b)
{
uint size = (uint)b.Length;

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

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
@ -17,13 +18,42 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
private readonly ITestOutputHelper output;
public UniformUnmanagedMemoryPoolTests(ITestOutputHelper output)
public UniformUnmanagedMemoryPoolTests(ITestOutputHelper output) => this.output = output;
private class CleanupUtil : IDisposable
{
this.output = output;
}
private readonly UniformUnmanagedMemoryPool pool;
private readonly List<UnmanagedMemoryHandle> handlesToDestroy = new();
private readonly List<IntPtr> ptrsToDestroy = new();
public CleanupUtil(UniformUnmanagedMemoryPool pool)
{
this.pool = pool;
}
public void Register(UnmanagedMemoryHandle handle) => this.handlesToDestroy.Add(handle);
private static unsafe Span<byte> GetSpan(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) =>
new Span<byte>((void*)h.DangerousGetHandle(), pool.BufferLength);
public void Register(IEnumerable<UnmanagedMemoryHandle> handles) => this.handlesToDestroy.AddRange(handles);
public void Register(IntPtr memoryPtr) => this.ptrsToDestroy.Add(memoryPtr);
public void Register(IEnumerable<IntPtr> memoryPtrs) => this.ptrsToDestroy.AddRange(memoryPtrs);
public void Dispose()
{
foreach (UnmanagedMemoryHandle handle in this.handlesToDestroy)
{
handle.Free();
}
this.pool.Release();
foreach (IntPtr ptr in this.ptrsToDestroy)
{
Marshal.FreeHGlobal(ptr);
}
}
}
[Theory]
[InlineData(3, 11)]
@ -41,10 +71,13 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_SingleBuffer_ReturnsCorrectBuffer(int length, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(length, capacity);
using var cleanup = new CleanupUtil(pool);
for (int i = 0; i < capacity; i++)
{
UnmanagedMemoryHandle h = pool.Rent();
CheckBuffer(length, pool, h);
cleanup.Register(h);
}
}
@ -68,9 +101,8 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
private static void CheckBuffer(int length, UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h)
{
Assert.NotNull(h);
Assert.False(h.IsClosed);
Span<byte> span = GetSpan(pool, h);
Assert.False(h.IsInvalid);
Span<byte> span = h.GetSpan();
span.Fill(123);
byte[] expected = new byte[length];
@ -86,7 +118,10 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_MultiBuffer_ReturnsCorrectBuffers(int length, int bufferCount)
{
var pool = new UniformUnmanagedMemoryPool(length, 10);
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] handles = pool.Rent(bufferCount);
cleanup.Register(handles);
Assert.NotNull(handles);
Assert.Equal(bufferCount, handles.Length);
@ -100,12 +135,15 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_MultipleTimesWithoutReturn_ReturnsDifferentHandles()
{
var pool = new UniformUnmanagedMemoryPool(128, 10);
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] a = pool.Rent(2);
cleanup.Register(a);
UnmanagedMemoryHandle b = pool.Rent();
cleanup.Register(b);
Assert.NotEqual(a[0].DangerousGetHandle(), a[1].DangerousGetHandle());
Assert.NotEqual(a[0].DangerousGetHandle(), b.DangerousGetHandle());
Assert.NotEqual(a[1].DangerousGetHandle(), b.DangerousGetHandle());
Assert.NotEqual(a[0].Handle, a[1].Handle);
Assert.NotEqual(a[0].Handle, b.Handle);
Assert.NotEqual(a[1].Handle, b.Handle);
}
[Theory]
@ -115,6 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void RentReturnRent_SameBuffers(int totalCount, int rentUnit, int capacity)
{
var pool = new UniformUnmanagedMemoryPool(128, capacity);
using var cleanup = new CleanupUtil(pool);
var allHandles = new HashSet<UnmanagedMemoryHandle>();
var handleUnits = new List<UnmanagedMemoryHandle[]>();
@ -128,6 +167,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
allHandles.Add(array);
}
// Allocate some memory, so potential new pool allocation wouldn't allocated the same memory:
cleanup.Register(Marshal.AllocHGlobal(128));
}
foreach (UnmanagedMemoryHandle[] arrayUnit in handleUnits)
@ -151,14 +193,20 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
Assert.Contains(array, allHandles);
}
cleanup.Register(allHandles);
}
[Fact]
public void Rent_SingleBuffer_OverCapacity_ReturnsNull()
public void Rent_SingleBuffer_OverCapacity_ReturnsInvalidBuffer()
{
var pool = new UniformUnmanagedMemoryPool(7, 1000);
Assert.NotNull(pool.Rent(1000));
Assert.Null(pool.Rent());
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] initial = pool.Rent(1000);
Assert.NotNull(initial);
cleanup.Register(initial);
UnmanagedMemoryHandle b1 = pool.Rent();
Assert.True(b1.IsInvalid);
}
[Theory]
@ -168,8 +216,12 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
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));
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] initial = pool.Rent(initialRent);
Assert.NotNull(initial);
cleanup.Register(initial);
UnmanagedMemoryHandle[] b1 = pool.Rent(attempt);
Assert.Null(b1);
}
[Theory]
@ -180,56 +232,49 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
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));
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] b0 = pool.Rent(initialRent);
Assert.NotNull(b0);
cleanup.Register(b0);
UnmanagedMemoryHandle[] b1 = pool.Rent(attempt);
Assert.NotNull(b1);
cleanup.Register(b1);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Release_SubsequentRentReturnsNull(bool multiple)
public void RentReturnRelease_SubsequentRentReturnsDifferentHandles(bool multiple)
{
var pool = new UniformUnmanagedMemoryPool(16, 16);
pool.Rent(); // Dummy rent
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle b0 = pool.Rent();
IntPtr h0 = b0.Handle;
UnmanagedMemoryHandle b1 = pool.Rent();
IntPtr h1 = b1.Handle;
pool.Return(b0);
pool.Return(b1);
pool.Release();
// Do some unmanaged allocations to make sure new pool buffers are different:
IntPtr[] dummy = Enumerable.Range(0, 100).Select(_ => Marshal.AllocHGlobal(16)).ToArray();
cleanup.Register(dummy);
if (multiple)
{
UnmanagedMemoryHandle b = pool.Rent();
Assert.Null(b);
cleanup.Register(b);
Assert.NotEqual(h0, b.Handle);
Assert.NotEqual(h1, b.Handle);
}
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);
cleanup.Register(b);
Assert.NotEqual(h0, b[0].Handle);
Assert.NotEqual(h1, b[0].Handle);
Assert.NotEqual(h0, b[1].Handle);
Assert.NotEqual(h1, b[1].Handle);
}
}
@ -257,6 +302,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
int count = Environment.ProcessorCount * 200;
var pool = new UniformUnmanagedMemoryPool(8, count);
using var cleanup = new CleanupUtil(pool);
var rnd = new Random(0);
Parallel.For(0, Environment.ProcessorCount, (int i) =>
@ -267,8 +313,8 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
UnmanagedMemoryHandle[] data = pool.Rent(2);
GetSpan(pool, data[0]).Fill((byte)i);
GetSpan(pool, data[1]).Fill((byte)i);
data[0].GetSpan().Fill((byte)i);
data[1].GetSpan().Fill((byte)i);
allArrays.Add(data[0]);
allArrays.Add(data[1]);
@ -283,7 +329,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
foreach (UnmanagedMemoryHandle array in allArrays)
{
Assert.True(expected.SequenceEqual(GetSpan(pool, array)));
Assert.True(expected.SequenceEqual(array.GetSpan()));
pool.Return(new[] { array });
}
});

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

@ -247,6 +247,8 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles);
b.Dispose();
g.Dispose();
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles);
allocator.ReleaseRetainedResources();
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
@ -307,7 +309,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[ConditionalTheory(nameof(IsWindows))]
[InlineData(300)]
[InlineData(600)]
[InlineData(1200)]
public void MemoryOwnerFinalizer_ReturnsToPool(int length)
{
// RunTest(length.ToString());
@ -332,9 +333,11 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
using IMemoryOwner<byte> g = allocator.Allocate<byte>(lengthInner);
Assert.Equal(42, g.GetSpan()[0]);
GC.KeepAlive(allocator);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
{
IMemoryOwner<byte> g = allocator.Allocate<byte>(length);

93
tests/ImageSharp.Tests/Memory/Allocators/UnmanagedBufferTests.cs

@ -0,0 +1,93 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Collections.Generic;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Memory.Internals;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
public class UnmanagedBufferTests
{
public class AllocatorBufferTests : BufferTestSuite
{
public AllocatorBufferTests()
: base(new UnmanagedMemoryAllocator(1024 * 64))
{
}
}
[Fact]
public void Allocate_CreatesValidBuffer()
{
using var buffer = UnmanagedBuffer<int>.Allocate(10);
Span<int> span = buffer.GetSpan();
Assert.Equal(10, span.Length);
span[9] = 123;
Assert.Equal(123, span[9]);
}
[Fact]
public unsafe void Dispose_DoesNotReleaseOutstandingReferences()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
var buffer = UnmanagedBuffer<int>.Allocate(10);
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
Span<int> span = buffer.GetSpan();
// Pin should AddRef
using (MemoryHandle h = buffer.Pin())
{
int* ptr = (int*)h.Pointer;
((IDisposable)buffer).Dispose();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
ptr[3] = 13;
Assert.Equal(13, span[3]);
} // Unpin should ReleaseRef
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
}
}
[Theory]
[InlineData(2)]
[InlineData(12)]
public void BufferFinalization_TracksAllocations(int count)
{
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose();
static void RunTest(string countStr)
{
int countInner = int.Parse(countStr);
List<UnmanagedBuffer<byte>> 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<UnmanagedBuffer<byte>> FillList(int countInner)
{
var l = new List<UnmanagedBuffer<byte>>();
for (int i = 0; i < countInner; i++)
{
var h = UnmanagedBuffer<byte>.Allocate(42);
l.Add(h);
}
return l;
}
}
}
}

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

@ -14,10 +14,10 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[Fact]
public unsafe void Allocate_AllocatesReadWriteMemory()
{
using var h = UnmanagedMemoryHandle.Allocate(128);
Assert.False(h.IsClosed);
var h = UnmanagedMemoryHandle.Allocate(128);
Assert.False(h.IsInvalid);
byte* ptr = (byte*)h.DangerousGetHandle();
Assert.True(h.IsValid);
byte* ptr = (byte*)h.Handle;
for (int i = 0; i < 128; i++)
{
ptr[i] = (byte)i;
@ -27,21 +27,23 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{
Assert.Equal((byte)i, ptr[i]);
}
h.Free();
}
[Fact]
public void Dispose_ClosesHandle()
public void Free_ClosesHandle()
{
var h = UnmanagedMemoryHandle.Allocate(128);
h.Dispose();
Assert.True(h.IsClosed);
h.Free();
Assert.True(h.IsInvalid);
Assert.Equal(IntPtr.Zero, h.Handle);
}
[Theory]
[InlineData(1)]
[InlineData(13)]
public void CreateDispose_TracksAllocations(int count)
public void Create_Free_AllocationsAreTracked(int count)
{
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose();
@ -60,125 +62,39 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
for (int i = 0; i < countInner; i++)
{
Assert.Equal(countInner - i, UnmanagedMemoryHandle.TotalOutstandingHandles);
l[i].Dispose();
l[i].Free();
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 = UnmanagedMemoryHandle.Allocate(42);
l.Add(h);
}
return l;
}
}
[Fact]
public void Resurrect_PreventsFinalization()
public void Equality_WhenTrue()
{
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 = UnmanagedMemoryHandle.Allocate(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();
}
var h1 = UnmanagedMemoryHandle.Allocate(10);
UnmanagedMemoryHandle h2 = h1;
Assert.True(h1.Equals(h2));
Assert.True(h2.Equals(h1));
Assert.True(h1 == h2);
Assert.False(h1 != h2);
Assert.True(h1.GetHashCode() == h2.GetHashCode());
h1.Free();
}
[Fact]
public void AssignedToNewOwner_ReRegistersForFinalization()
public void Equality_WhenFalse()
{
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(UnmanagedMemoryHandle.Allocate(42));
}
var h1 = UnmanagedMemoryHandle.Allocate(10);
var h2 = UnmanagedMemoryHandle.Allocate(10);
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);
}
Assert.False(h1.Equals(h2));
Assert.False(h2.Equals(h1));
Assert.False(h1 == h2);
Assert.True(h1 != h2);
resurrectedHandle = null;
}
h1.Free();
h2.Free();
}
}
}

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

@ -19,8 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
public class Allocate : MemoryGroupTestsBase
{
#pragma warning disable SA1509
public static TheoryData<object, int, int, long, int, int, int> AllocateData =
new TheoryData<object, int, int, long, int, int, int>()
public static TheoryData<object, int, int, long, int, int, int> AllocateData = new()
{
{ default(S5), 22, 4, 4, 1, 4, 4 },
{ default(S5), 22, 4, 7, 2, 4, 3 },
@ -100,9 +99,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
g.Dispose();
}
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)]
@ -112,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
UnmanagedMemoryHandle[] buffers = pool.Rent(5);
foreach (UnmanagedMemoryHandle b in buffers)
{
GetSpan(pool, b).Fill(42);
b.GetSpan().Fill(42);
}
pool.Return(buffers);

2
tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs

@ -61,8 +61,6 @@ namespace SixLabors.ImageSharp.Tests
}
});
return result;
}

Loading…
Cancel
Save