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> /// <summary>
/// Verifies, that the target span is of same size than the 'other' span. /// Verifies, that the target span is of same size than the 'other' span.
/// </summary> /// </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;
using System.Buffers; using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory.Internals namespace SixLabors.ImageSharp.Memory.Internals
{ {
internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T> internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>, IRefCounted
where T : struct where T : struct
{ {
private readonly int lengthInBytes; private readonly int lengthInBytes;
private byte[] array; private byte[] array;
private LifetimeGuard lifetimeGuard;
public SharedArrayPoolBuffer(int lengthInElements) public SharedArrayPoolBuffer(int lengthInElements)
{ {
this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>(); this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>();
this.array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes); 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) protected override void Dispose(bool disposing)
{ {
if (this.array == null) if (this.array == null)
@ -34,12 +30,51 @@ namespace SixLabors.ImageSharp.Memory.Internals
return; return;
} }
ArrayPool<byte>.Shared.Return(this.array); this.lifetimeGuard.Dispose();
this.array = null; 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; 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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals namespace SixLabors.ImageSharp.Memory.Internals
{ {
internal partial class UniformUnmanagedMemoryPool 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 static readonly Stopwatch Stopwatch = Stopwatch.StartNew();
private readonly TrimSettings trimSettings; private readonly TrimSettings trimSettings;
private UnmanagedMemoryHandle[] buffers; private readonly UnmanagedMemoryHandle[] buffers;
private int index; private int index;
private Timer trimTimer;
private long lastTrimTimestamp; private long lastTrimTimestamp;
public UniformUnmanagedMemoryPool(int bufferLength, int capacity) public UniformUnmanagedMemoryPool(int bufferLength, int capacity)
@ -31,16 +36,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
if (trimSettings.Enabled) if (trimSettings.Enabled)
{ {
// Invoke the timer callback more frequently, than trimSettings.TrimPeriodMilliseconds, UpdateTimer(trimSettings, this);
// 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);
#if NETCORE31COMPATIBLE #if NETCORE31COMPATIBLE
Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this); Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this);
#endif #endif
@ -52,31 +48,33 @@ namespace SixLabors.ImageSharp.Memory.Internals
public int Capacity { get; } public int Capacity { get; }
/// <summary>
/// Rent a single buffer or return <see cref="UnmanagedMemoryHandle.NullHandle"/> if the pool is full.
/// </summary>
public UnmanagedMemoryHandle Rent() public UnmanagedMemoryHandle Rent()
{ {
UnmanagedMemoryHandle[] buffersLocal = this.buffers; UnmanagedMemoryHandle[] buffersLocal = this.buffers;
// Avoid taking the lock if the pool is released or is over limit: // Avoid taking the lock if the pool is is over it's limit:
if (buffersLocal == null || this.index == buffersLocal.Length) if (this.index == buffersLocal.Length)
{ {
return null; return UnmanagedMemoryHandle.NullHandle;
} }
UnmanagedMemoryHandle buffer; UnmanagedMemoryHandle buffer;
lock (buffersLocal) lock (buffersLocal)
{ {
// Check again after taking the lock: // 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]; buffer = buffersLocal[this.index];
buffersLocal[this.index++] = null; buffersLocal[this.index++] = default;
} }
if (buffer == null) if (buffer.IsInvalid)
{ {
buffer = UnmanagedMemoryHandle.Allocate(this.BufferLength); buffer = UnmanagedMemoryHandle.Allocate(this.BufferLength);
} }
@ -84,12 +82,15 @@ namespace SixLabors.ImageSharp.Memory.Internals
return buffer; 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; UnmanagedMemoryHandle[] buffersLocal = this.buffers;
// Avoid taking the lock if the pool is released or is over limit: // Avoid taking the lock if the pool is is over it's limit:
if (buffersLocal == null || this.index + bufferCount >= buffersLocal.Length + 1) if (this.index + bufferCount >= buffersLocal.Length + 1)
{ {
return null; return null;
} }
@ -98,7 +99,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
lock (buffersLocal) lock (buffersLocal)
{ {
// Check again after taking the lock: // 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; return null;
} }
@ -107,128 +108,138 @@ namespace SixLabors.ImageSharp.Memory.Internals
for (int i = 0; i < bufferCount; i++) for (int i = 0; i < bufferCount; i++)
{ {
result[i] = buffersLocal[this.index]; result[i] = buffersLocal[this.index];
buffersLocal[this.index++] = null; buffersLocal[this.index++] = UnmanagedMemoryHandle.NullHandle;
} }
} }
for (int i = 0; i < result.Length; i++) for (int i = 0; i < result.Length; i++)
{ {
if (result[i] == null) if (result[i].IsInvalid)
{ {
result[i] = UnmanagedMemoryHandle.Allocate(this.BufferLength); result[i] = UnmanagedMemoryHandle.Allocate(this.BufferLength);
} }
if (allocationOptions.Has(AllocationOptions.Clean))
{
this.GetSpan(result[i]).Clear();
}
} }
return result; return result;
} }
public void Return(UnmanagedMemoryHandle buffer) public void Return(UnmanagedMemoryHandle bufferHandle)
{ {
UnmanagedMemoryHandle[] buffersLocal = this.buffers; Guard.IsTrue(bufferHandle.IsValid, nameof(bufferHandle), "Returning NullHandle to the pool is not allowed.");
if (buffersLocal == null) lock (this.buffers)
{
buffer.Dispose();
return;
}
lock (buffersLocal)
{ {
// Check again after taking the lock: // Check again after taking the lock:
if (this.buffers == null)
{
buffer.Dispose();
return;
}
if (this.index == 0) if (this.index == 0)
{ {
ThrowReturnedMoreBuffersThanRented(); // DEBUG-only exception ThrowReturnedMoreBuffersThanRented(); // DEBUG-only exception
buffer.Dispose(); bufferHandle.Free();
return; 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; lock (this.buffers)
if (buffersLocal == null)
{
DisposeAll(buffers);
return;
}
lock (buffersLocal)
{ {
// Check again after taking the lock: if (this.index - bufferHandles.Length + 1 <= 0)
if (this.buffers == null)
{
DisposeAll(buffers);
return;
}
if (this.index - buffers.Length + 1 <= 0)
{ {
ThrowReturnedMoreBuffersThanRented(); ThrowReturnedMoreBuffersThanRented();
DisposeAll(buffers); DisposeAll(bufferHandles);
return; 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() public void Release()
{ {
this.trimTimer?.Dispose(); lock (this.buffers)
this.trimTimer = null; {
UnmanagedMemoryHandle[] oldBuffers = Interlocked.Exchange(ref this.buffers, null); for (int i = this.index; i < this.buffers.Length; i++)
DebugGuard.NotNull(oldBuffers, nameof(oldBuffers)); {
DisposeAll(oldBuffers); UnmanagedMemoryHandle buffer = this.buffers[i];
if (buffer.IsInvalid)
{
break;
}
buffer.Free();
this.buffers[i] = UnmanagedMemoryHandle.NullHandle;
}
}
} }
private static void DisposeAll(Span<UnmanagedMemoryHandle> buffers) private static void DisposeAll(Span<UnmanagedMemoryHandle> buffers)
{ {
foreach (UnmanagedMemoryHandle handle in 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, // This indicates a bug in the library, however Return() might be called from a finalizer,
// therefore we should never throw here in production. // therefore we should never throw here in production.
[Conditional("DEBUG")] [Conditional("DEBUG")]
private static void ThrowReturnedMoreBuffersThanRented() => private static void ThrowReturnedMoreBuffersThanRented() =>
throw new InvalidMemoryOperationException("Returned more buffers then rented"); 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; lock (AllPools)
if (buffersLocal == null)
{ {
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(); bool isHighPressure = this.IsHighMemoryPressure();
@ -250,16 +261,11 @@ namespace SixLabors.ImageSharp.Memory.Internals
{ {
lock (buffersLocal) lock (buffersLocal)
{ {
if (this.buffers == null)
{
return false;
}
// Trim all: // 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].Free();
buffersLocal[i] = null; buffersLocal[i] = UnmanagedMemoryHandle.NullHandle;
} }
} }
@ -270,14 +276,9 @@ namespace SixLabors.ImageSharp.Memory.Internals
{ {
lock (buffersLocal) lock (buffersLocal)
{ {
if (this.buffers == null)
{
return false;
}
// Count the buffers in the pool: // Count the buffers in the pool:
int retainedCount = 0; 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++; retainedCount++;
} }
@ -288,8 +289,8 @@ namespace SixLabors.ImageSharp.Memory.Internals
int trimStop = this.index + retainedCount - trimCount; int trimStop = this.index + retainedCount - trimCount;
for (int i = trimStart; i >= trimStop; i--) for (int i = trimStart; i >= trimStop; i--)
{ {
buffersLocal[i].Dispose(); buffersLocal[i].Free();
buffersLocal[i] = null; buffersLocal[i] = UnmanagedMemoryHandle.NullHandle;
} }
this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds; 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.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals 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)"/>. /// access to unmanaged buffers allocated by <see cref="Marshal.AllocHGlobal(int)"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The element type.</typeparam> /// <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 where T : struct
{ {
private readonly int lengthInElements; private readonly int lengthInElements;
/// <summary> private readonly UnmanagedBufferLifetimeGuard lifetimeGuard;
/// Initializes a new instance of the <see cref="UnmanagedBuffer{T}"/> class.
/// </summary> private int disposed;
/// <param name="lengthInElements">The number of elements to allocate.</param>
public UnmanagedBuffer(int lengthInElements)
: this(UnmanagedMemoryHandle.Allocate(lengthInElements * Unsafe.SizeOf<T>()), lengthInElements)
{
}
protected UnmanagedBuffer(UnmanagedMemoryHandle bufferHandle, int lengthInElements) public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifetimeGuard)
{ {
DebugGuard.NotNull(lifetimeGuard, nameof(lifetimeGuard));
this.lengthInElements = lengthInElements; this.lengthInElements = lengthInElements;
this.BufferHandle = bufferHandle; this.lifetimeGuard = lifetimeGuard;
} }
public UnmanagedMemoryHandle BufferHandle { get; protected set; } private void* Pointer => this.lifetimeGuard.Handle.Pointer;
private void* Pointer => (void*)this.BufferHandle.DangerousGetHandle();
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 /> /// <inheritdoc />
public override MemoryHandle Pin(int elementIndex = 0) 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 // Will be released in Unpin
bool unused = false; this.lifetimeGuard.AddRef();
this.BufferHandle.DangerousAddRef(ref unused);
void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex); void* pbData = Unsafe.Add<T>(this.Pointer, elementIndex);
return new MemoryHandle(pbData, pinnable: this); return new MemoryHandle(pbData, pinnable: this);
} }
/// <inheritdoc />
public override void Unpin() => this.BufferHandle.DangerousRelease();
/// <inheritdoc /> /// <inheritdoc />
protected override void Dispose(bool disposing) 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; return;
} }
if (disposing) this.lifetimeGuard.Dispose();
{
this.BufferHandle.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. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace SixLabors.ImageSharp.Memory.Internals 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 const int MaxAllocationAttempts = 1000;
private readonly int lengthInBytes;
private bool resurrected;
// Track allocations for testing purposes: // Track allocations for testing purposes:
private static int totalOutstandingHandles; private static int totalOutstandingHandles;
@ -24,10 +24,16 @@ namespace SixLabors.ImageSharp.Memory.Internals
// A Monitor to wait/signal when we are low on memory. // A Monitor to wait/signal when we are low on memory.
private static object lowMemoryMonitor; private static object lowMemoryMonitor;
public static readonly UnmanagedMemoryHandle NullHandle = default;
private IntPtr handle;
private readonly int lengthInBytes;
private UnmanagedMemoryHandle(IntPtr handle, int lengthInBytes) private UnmanagedMemoryHandle(IntPtr handle, int lengthInBytes)
: base(handle, true)
{ {
this.handle = handle;
this.lengthInBytes = lengthInBytes; this.lengthInBytes = lengthInBytes;
if (lengthInBytes > 0) if (lengthInBytes > 0)
{ {
GC.AddMemoryPressure(lengthInBytes); GC.AddMemoryPressure(lengthInBytes);
@ -36,6 +42,14 @@ namespace SixLabors.ImageSharp.Memory.Internals
Interlocked.Increment(ref totalOutstandingHandles); 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> /// <summary>
/// Gets the total outstanding handle allocations for testing purposes. /// Gets the total outstanding handle allocations for testing purposes.
/// </summary> /// </summary>
@ -46,36 +60,34 @@ namespace SixLabors.ImageSharp.Memory.Internals
/// </summary> /// </summary>
internal static long TotalOomRetries => totalOomRetries; internal static long TotalOomRetries => totalOomRetries;
/// <inheritdoc /> public static bool operator ==(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => a.Equals(b);
public override bool IsInvalid => this.handle == IntPtr.Zero;
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) if (this.IsInvalid)
{ {
return false; ThrowDisposed();
} }
Marshal.FreeHGlobal(this.handle); return new Span<byte>(this.Pointer, this.lengthInBytes);
if (this.lengthInBytes > 0) }
{
GC.RemoveMemoryPressure(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(). ThrowDisposed();
Monitor.Enter(lowMemoryMonitor);
Monitor.PulseAll(lowMemoryMonitor);
Monitor.Exit(lowMemoryMonitor);
} }
this.handle = IntPtr.Zero; return new Span<byte>(this.Pointer, lengthInBytes);
Interlocked.Decrement(ref totalOutstandingHandles);
return true;
} }
internal static UnmanagedMemoryHandle Allocate(int lengthInBytes) public static UnmanagedMemoryHandle Allocate(int lengthInBytes)
{ {
IntPtr handle = AllocateHandle(lengthInBytes); IntPtr handle = AllocateHandle(lengthInBytes);
return new UnmanagedMemoryHandle(handle, lengthInBytes); return new UnmanagedMemoryHandle(handle, lengthInBytes);
@ -115,26 +127,38 @@ namespace SixLabors.ImageSharp.Memory.Internals
return handle; return handle;
} }
/// <summary> public void Free()
/// 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); IntPtr h = Interlocked.Exchange(ref this.handle, IntPtr.Zero);
this.resurrected = true;
}
internal void AssignedToNewOwner() if (h == IntPtr.Zero)
{ {
if (this.resurrected) 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 // We are low on memory. Signal all threads waiting in AllocateHandle().
GC.ReRegisterForFinalize(this); Monitor.Enter(lowMemoryMonitor);
this.resurrected = false; 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> /// </summary>
public abstract class MemoryAllocator public abstract class MemoryAllocator
{ {
private static MemoryAllocator defaultMemoryAllocator = Create();
/// <summary> /// <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> /// </summary>
/// <remarks> public static MemoryAllocator Default { get; } = Create();
/// 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;
}
}
/// <summary> /// <summary>
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes. /// 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) if (lengthInBytes <= this.poolBufferSizeInBytes)
{ {
UnmanagedMemoryHandle array = this.pool.Rent(); UnmanagedMemoryHandle mem = this.pool.Rent();
if (array != null) if (mem.IsValid)
{ {
var buffer = new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, length); UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options);
if (options.Has(AllocationOptions.Clean))
{
buffer.Clear();
}
return buffer; return buffer;
} }
} }
@ -130,15 +125,10 @@ namespace SixLabors.ImageSharp.Memory
if (totalLengthInBytes <= this.poolBufferSizeInBytes) if (totalLengthInBytes <= this.poolBufferSizeInBytes)
{ {
// Optimized path renting single array from the pool // Optimized path renting single array from the pool
UnmanagedMemoryHandle array = this.pool.Rent(); UnmanagedMemoryHandle mem = this.pool.Rent();
if (array != null) if (mem.IsValid)
{ {
var buffer = new UniformUnmanagedMemoryPool.FinalizableBuffer<T>(this.pool, array, (int)totalLength); UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, (int)totalLength, options);
if (options.Has(AllocationOptions.Clean))
{
buffer.Clear();
}
return MemoryGroup<T>.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); 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); return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options);
} }
public override void ReleaseRetainedResources() public override void ReleaseRetainedResources() => this.pool.Release();
{
UniformUnmanagedMemoryPool oldPool = Interlocked.Exchange(
ref this.pool,
new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity, this.trimSettings));
oldPool.Release();
}
private static long GetDefaultMaxPoolSizeBytes() 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) 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)) if (options.Has(AllocationOptions.Clean))
{ {
buffer.GetSpan().Clear(); 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>> public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>>
{ {
private IMemoryOwner<T>[] memoryOwners; private IMemoryOwner<T>[] memoryOwners;
private byte[][] pooledArrays; private RefCountedLifetimeGuard groupLifetimeGuard;
private UniformUnmanagedMemoryPool unmanagedMemoryPool;
private UnmanagedMemoryHandle[] pooledHandles;
public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable) public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable)
: base(bufferLength, totalLength) : base(bufferLength, totalLength)
@ -30,14 +28,15 @@ namespace SixLabors.ImageSharp.Memory
this.View = new MemoryGroupView<T>(this); this.View = new MemoryGroupView<T>(this);
} }
public Owned(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] pooledArrays, int bufferLength, long totalLength, int sizeOfLastBuffer, AllocationOptions options) public Owned(
: this(CreateBuffers(pool, pooledArrays, bufferLength, sizeOfLastBuffer, options), bufferLength, totalLength, true) UniformUnmanagedMemoryPool pool,
{ UnmanagedMemoryHandle[] pooledHandles,
this.pooledHandles = pooledArrays; int bufferLength,
this.unmanagedMemoryPool = pool; long totalLength,
} int sizeOfLastBuffer,
AllocationOptions options)
~Owned() => this.Dispose(false); : this(CreateBuffers(pooledHandles, bufferLength, sizeOfLastBuffer, options), bufferLength, totalLength, true) =>
this.groupLifetimeGuard = pool.CreateGroupLifetimeGuard(pooledHandles);
public bool Swappable { get; } public bool Swappable { get; }
@ -63,7 +62,6 @@ namespace SixLabors.ImageSharp.Memory
} }
private static IMemoryOwner<T>[] CreateBuffers( private static IMemoryOwner<T>[] CreateBuffers(
UniformUnmanagedMemoryPool pool,
UnmanagedMemoryHandle[] pooledBuffers, UnmanagedMemoryHandle[] pooledBuffers,
int bufferLength, int bufferLength,
int sizeOfLastBuffer, int sizeOfLastBuffer,
@ -72,42 +70,35 @@ namespace SixLabors.ImageSharp.Memory
var result = new IMemoryOwner<T>[pooledBuffers.Length]; var result = new IMemoryOwner<T>[pooledBuffers.Length];
for (int i = 0; i < pooledBuffers.Length - 1; i++) for (int i = 0; i < pooledBuffers.Length - 1; i++)
{ {
pooledBuffers[i].AssignedToNewOwner(); var currentBuffer = ObservedBuffer.Create(pooledBuffers[i], bufferLength, options);
var currentBuffer = new UniformUnmanagedMemoryPool.Buffer<T>(pool, pooledBuffers[i], bufferLength);
if (options.Has(AllocationOptions.Clean))
{
currentBuffer.Clear();
}
result[i] = currentBuffer; result[i] = currentBuffer;
} }
var lastBuffer = new UniformUnmanagedMemoryPool.Buffer<T>(pool, pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer); var lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options);
if (options.Has(AllocationOptions.Clean))
{
lastBuffer.Clear();
}
result[result.Length - 1] = lastBuffer; result[result.Length - 1] = lastBuffer;
return result; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator() public override MemoryGroupEnumerator<T> GetEnumerator() => new(this);
{
return new MemoryGroupEnumerator<T>(this);
}
public override void IncreaseRefCounts() public override void IncreaseRefCounts()
{ {
this.EnsureNotDisposed(); 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() public override void DecreaseRefCounts()
{ {
this.EnsureNotDisposed(); 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) protected override void Dispose(bool disposing)
{ {
if (this.IsDisposed) if (this.IsDisposed || !disposing)
{ {
return; return;
} }
this.View.Invalidate(); this.View.Invalidate();
if (this.unmanagedMemoryPool != null) if (this.groupLifetimeGuard != null)
{ {
this.unmanagedMemoryPool.Return(this.pooledHandles); this.groupLifetimeGuard.Dispose();
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);
} }
else if (disposing) else
{ {
foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners) foreach (IMemoryOwner<T> memoryOwner in this.memoryOwners)
{ {
@ -170,9 +152,7 @@ namespace SixLabors.ImageSharp.Memory
this.memoryOwners = null; this.memoryOwners = null;
this.IsValid = false; this.IsValid = false;
this.pooledArrays = null; this.groupLifetimeGuard = null;
this.unmanagedMemoryPool = null;
this.pooledHandles = null;
} }
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
@ -195,29 +175,67 @@ namespace SixLabors.ImageSharp.Memory
IMemoryOwner<T>[] tempOwners = a.memoryOwners; IMemoryOwner<T>[] tempOwners = a.memoryOwners;
long tempTotalLength = a.TotalLength; long tempTotalLength = a.TotalLength;
int tempBufferLength = a.BufferLength; int tempBufferLength = a.BufferLength;
byte[][] tempPooledArrays = a.pooledArrays; RefCountedLifetimeGuard tempGroupOwner = a.groupLifetimeGuard;
UniformUnmanagedMemoryPool tempUnmangedPool = a.unmanagedMemoryPool;
UnmanagedMemoryHandle[] tempPooledHandles = a.pooledHandles;
a.memoryOwners = b.memoryOwners; a.memoryOwners = b.memoryOwners;
a.TotalLength = b.TotalLength; a.TotalLength = b.TotalLength;
a.BufferLength = b.BufferLength; a.BufferLength = b.BufferLength;
a.pooledArrays = b.pooledArrays; a.groupLifetimeGuard = b.groupLifetimeGuard;
a.unmanagedMemoryPool = b.unmanagedMemoryPool;
a.pooledHandles = b.pooledHandles;
b.memoryOwners = tempOwners; b.memoryOwners = tempOwners;
b.TotalLength = tempTotalLength; b.TotalLength = tempTotalLength;
b.BufferLength = tempBufferLength; b.BufferLength = tempBufferLength;
b.pooledArrays = tempPooledArrays; b.groupLifetimeGuard = tempGroupOwner;
b.unmanagedMemoryPool = tempUnmangedPool;
b.pooledHandles = tempPooledHandles;
a.View.Invalidate(); a.View.Invalidate();
b.View.Invalidate(); b.View.Invalidate();
a.View = new MemoryGroupView<T>(a); a.View = new MemoryGroupView<T>(a);
b.View = new MemoryGroupView<T>(b); 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++; bufferCount++;
} }
UnmanagedMemoryHandle[] arrays = pool.Rent(bufferCount, options); UnmanagedMemoryHandle[] arrays = pool.Rent(bufferCount);
if (arrays == null) if (arrays == null)
{ {

11
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -156,17 +156,14 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest() static void RunTest()
{ {
MemoryAllocator allocator = new TestMemoryAllocator();
MemoryAllocator.Default = allocator;
var c1 = new Configuration(); var c1 = new Configuration();
var c2 = new Configuration(new MockConfigurationModule()); var c2 = new Configuration(new MockConfigurationModule());
var c3 = Configuration.CreateDefaultInstance(); var c3 = Configuration.CreateDefaultInstance();
Assert.Same(allocator, Configuration.Default.MemoryAllocator); Assert.Same(MemoryAllocator.Default, Configuration.Default.MemoryAllocator);
Assert.Same(allocator, c1.MemoryAllocator); Assert.Same(MemoryAllocator.Default, c1.MemoryAllocator);
Assert.Same(allocator, c2.MemoryAllocator); Assert.Same(MemoryAllocator.Default, c2.MemoryAllocator);
Assert.Same(allocator, c3.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) static void RunTest(string testTypeName, string throwExceptionStr)
{ {
bool throwExceptionInner = bool.Parse(throwExceptionStr); bool throwExceptionInner = bool.Parse(throwExceptionStr);
var buffer = new UnmanagedBuffer<L8>(100); var buffer = UnmanagedBuffer<L8>.Allocate(100);
var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer); var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer);
Configuration.Default.MemoryAllocator = allocator; Configuration.Default.MemoryAllocator = allocator;
@ -192,7 +192,7 @@ namespace SixLabors.ImageSharp.Tests
{ {
GetTest(testTypeName).ProcessPixelRowsImpl(image, _ => GetTest(testTypeName).ProcessPixelRowsImpl(image, _ =>
{ {
buffer.BufferHandle.Dispose(); ((IDisposable)buffer).Dispose();
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles);
if (throwExceptionInner) if (throwExceptionInner)
{ {
@ -218,8 +218,8 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string testTypeName, string throwExceptionStr) static void RunTest(string testTypeName, string throwExceptionStr)
{ {
bool throwExceptionInner = bool.Parse(throwExceptionStr); bool throwExceptionInner = bool.Parse(throwExceptionStr);
var buffer1 = new UnmanagedBuffer<L8>(100); var buffer1 = UnmanagedBuffer<L8>.Allocate(100);
var buffer2 = new UnmanagedBuffer<L8>(100); var buffer2 = UnmanagedBuffer<L8>.Allocate(100);
var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer1, buffer2); var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer1, buffer2);
Configuration.Default.MemoryAllocator = allocator; Configuration.Default.MemoryAllocator = allocator;
@ -231,8 +231,8 @@ namespace SixLabors.ImageSharp.Tests
{ {
GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, (_, _) => GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, (_, _) =>
{ {
buffer1.BufferHandle.Dispose(); ((IDisposable)buffer1).Dispose();
buffer2.BufferHandle.Dispose(); ((IDisposable)buffer2).Dispose();
Assert.Equal(2, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.Equal(2, UnmanagedMemoryHandle.TotalOutstandingHandles);
if (throwExceptionInner) if (throwExceptionInner)
{ {
@ -258,9 +258,9 @@ namespace SixLabors.ImageSharp.Tests
static void RunTest(string testTypeName, string throwExceptionStr) static void RunTest(string testTypeName, string throwExceptionStr)
{ {
bool throwExceptionInner = bool.Parse(throwExceptionStr); bool throwExceptionInner = bool.Parse(throwExceptionStr);
var buffer1 = new UnmanagedBuffer<L8>(100); var buffer1 = UnmanagedBuffer<L8>.Allocate(100);
var buffer2 = new UnmanagedBuffer<L8>(100); var buffer2 = UnmanagedBuffer<L8>.Allocate(100);
var buffer3 = new UnmanagedBuffer<L8>(100); var buffer3 = UnmanagedBuffer<L8>.Allocate(100);
var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer1, buffer2, buffer3); var allocator = new MockUnmanagedMemoryAllocator<L8>(buffer1, buffer2, buffer3);
Configuration.Default.MemoryAllocator = allocator; Configuration.Default.MemoryAllocator = allocator;
@ -273,9 +273,9 @@ namespace SixLabors.ImageSharp.Tests
{ {
GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, image3, (_, _, _) => GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, image3, (_, _, _) =>
{ {
buffer1.BufferHandle.Dispose(); ((IDisposable)buffer1).Dispose();
buffer2.BufferHandle.Dispose(); ((IDisposable)buffer2).Dispose();
buffer3.BufferHandle.Dispose(); ((IDisposable)buffer3).Dispose();
Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles);
if (throwExceptionInner) if (throwExceptionInner)
{ {
@ -317,7 +317,7 @@ namespace SixLabors.ImageSharp.Tests
protected internal override int GetBufferCapacityInBytes() => int.MaxValue; protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) => 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading; using System.Threading;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Memory.Internals;
@ -13,14 +15,14 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
public partial class UniformUnmanagedMemoryPoolTests public partial class UniformUnmanagedMemoryPoolTests
{ {
[CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)]
public class NonParallelTests
{
}
[Collection(nameof(NonParallelTests))] [Collection(nameof(NonParallelTests))]
public class Trim public class Trim
{ {
[CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)]
public class NonParallelTests
{
}
[Fact] [Fact]
public void TrimPeriodElapsed_TrimsHalfOfUnusedArrays() 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 #if NETCORE31COMPATIBLE
public static readonly bool Is32BitProcess = !Environment.Is64BitProcess; 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))] [ConditionalFact(nameof(Is32BitProcess))]
public static void GC_Collect_OnHighLoad_TrimsEntirePool() public static void GC_Collect_OnHighLoad_TrimsEntirePool()
{ {
RemoteExecutor.Invoke(RunTest).Dispose(); RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest() static void RunTest()
{ {
Assert.False(Environment.Is64BitProcess); Assert.False(Environment.Is64BitProcess);
const int OneMb = 1024 * 1024; const int OneMb = 1 << 20;
var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { HighPressureThresholdRate = 0.2f }; var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { HighPressureThresholdRate = 0.2f };
@ -82,6 +122,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
// Prevent eager collection of the pool:
GC.KeepAlive(pool);
static void TouchPage(byte[] b) static void TouchPage(byte[] b)
{ {
uint size = (uint)b.Length; uint size = (uint)b.Length;

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

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
@ -17,13 +18,42 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
private readonly ITestOutputHelper output; 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) => public void Register(IEnumerable<UnmanagedMemoryHandle> handles) => this.handlesToDestroy.AddRange(handles);
new Span<byte>((void*)h.DangerousGetHandle(), pool.BufferLength);
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] [Theory]
[InlineData(3, 11)] [InlineData(3, 11)]
@ -41,10 +71,13 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_SingleBuffer_ReturnsCorrectBuffer(int length, int capacity) public void Rent_SingleBuffer_ReturnsCorrectBuffer(int length, int capacity)
{ {
var pool = new UniformUnmanagedMemoryPool(length, capacity); var pool = new UniformUnmanagedMemoryPool(length, capacity);
using var cleanup = new CleanupUtil(pool);
for (int i = 0; i < capacity; i++) for (int i = 0; i < capacity; i++)
{ {
UnmanagedMemoryHandle h = pool.Rent(); UnmanagedMemoryHandle h = pool.Rent();
CheckBuffer(length, pool, h); 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) private static void CheckBuffer(int length, UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h)
{ {
Assert.NotNull(h); Assert.False(h.IsInvalid);
Assert.False(h.IsClosed); Span<byte> span = h.GetSpan();
Span<byte> span = GetSpan(pool, h);
span.Fill(123); span.Fill(123);
byte[] expected = new byte[length]; byte[] expected = new byte[length];
@ -86,7 +118,10 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_MultiBuffer_ReturnsCorrectBuffers(int length, int bufferCount) public void Rent_MultiBuffer_ReturnsCorrectBuffers(int length, int bufferCount)
{ {
var pool = new UniformUnmanagedMemoryPool(length, 10); var pool = new UniformUnmanagedMemoryPool(length, 10);
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] handles = pool.Rent(bufferCount); UnmanagedMemoryHandle[] handles = pool.Rent(bufferCount);
cleanup.Register(handles);
Assert.NotNull(handles); Assert.NotNull(handles);
Assert.Equal(bufferCount, handles.Length); Assert.Equal(bufferCount, handles.Length);
@ -100,12 +135,15 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_MultipleTimesWithoutReturn_ReturnsDifferentHandles() public void Rent_MultipleTimesWithoutReturn_ReturnsDifferentHandles()
{ {
var pool = new UniformUnmanagedMemoryPool(128, 10); var pool = new UniformUnmanagedMemoryPool(128, 10);
using var cleanup = new CleanupUtil(pool);
UnmanagedMemoryHandle[] a = pool.Rent(2); UnmanagedMemoryHandle[] a = pool.Rent(2);
cleanup.Register(a);
UnmanagedMemoryHandle b = pool.Rent(); UnmanagedMemoryHandle b = pool.Rent();
cleanup.Register(b);
Assert.NotEqual(a[0].DangerousGetHandle(), a[1].DangerousGetHandle()); Assert.NotEqual(a[0].Handle, a[1].Handle);
Assert.NotEqual(a[0].DangerousGetHandle(), b.DangerousGetHandle()); Assert.NotEqual(a[0].Handle, b.Handle);
Assert.NotEqual(a[1].DangerousGetHandle(), b.DangerousGetHandle()); Assert.NotEqual(a[1].Handle, b.Handle);
} }
[Theory] [Theory]
@ -115,6 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void RentReturnRent_SameBuffers(int totalCount, int rentUnit, int capacity) public void RentReturnRent_SameBuffers(int totalCount, int rentUnit, int capacity)
{ {
var pool = new UniformUnmanagedMemoryPool(128, capacity); var pool = new UniformUnmanagedMemoryPool(128, capacity);
using var cleanup = new CleanupUtil(pool);
var allHandles = new HashSet<UnmanagedMemoryHandle>(); var allHandles = new HashSet<UnmanagedMemoryHandle>();
var handleUnits = new List<UnmanagedMemoryHandle[]>(); var handleUnits = new List<UnmanagedMemoryHandle[]>();
@ -128,6 +167,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
allHandles.Add(array); 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) foreach (UnmanagedMemoryHandle[] arrayUnit in handleUnits)
@ -151,14 +193,20 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
Assert.Contains(array, allHandles); Assert.Contains(array, allHandles);
} }
cleanup.Register(allHandles);
} }
[Fact] [Fact]
public void Rent_SingleBuffer_OverCapacity_ReturnsNull() public void Rent_SingleBuffer_OverCapacity_ReturnsInvalidBuffer()
{ {
var pool = new UniformUnmanagedMemoryPool(7, 1000); var pool = new UniformUnmanagedMemoryPool(7, 1000);
Assert.NotNull(pool.Rent(1000)); using var cleanup = new CleanupUtil(pool);
Assert.Null(pool.Rent()); UnmanagedMemoryHandle[] initial = pool.Rent(1000);
Assert.NotNull(initial);
cleanup.Register(initial);
UnmanagedMemoryHandle b1 = pool.Rent();
Assert.True(b1.IsInvalid);
} }
[Theory] [Theory]
@ -168,8 +216,12 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_MultiBuffer_OverCapacity_ReturnsNull(int initialRent, int attempt, int capacity) public void Rent_MultiBuffer_OverCapacity_ReturnsNull(int initialRent, int attempt, int capacity)
{ {
var pool = new UniformUnmanagedMemoryPool(128, capacity); var pool = new UniformUnmanagedMemoryPool(128, capacity);
Assert.NotNull(pool.Rent(initialRent)); using var cleanup = new CleanupUtil(pool);
Assert.Null(pool.Rent(attempt)); UnmanagedMemoryHandle[] initial = pool.Rent(initialRent);
Assert.NotNull(initial);
cleanup.Register(initial);
UnmanagedMemoryHandle[] b1 = pool.Rent(attempt);
Assert.Null(b1);
} }
[Theory] [Theory]
@ -180,56 +232,49 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
public void Rent_MultiBuff_BelowCapacity_Succeeds(int initialRent, int attempt, int capacity) public void Rent_MultiBuff_BelowCapacity_Succeeds(int initialRent, int attempt, int capacity)
{ {
var pool = new UniformUnmanagedMemoryPool(128, capacity); var pool = new UniformUnmanagedMemoryPool(128, capacity);
Assert.NotNull(pool.Rent(initialRent)); using var cleanup = new CleanupUtil(pool);
Assert.NotNull(pool.Rent(attempt)); UnmanagedMemoryHandle[] b0 = pool.Rent(initialRent);
Assert.NotNull(b0);
cleanup.Register(b0);
UnmanagedMemoryHandle[] b1 = pool.Rent(attempt);
Assert.NotNull(b1);
cleanup.Register(b1);
} }
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
public void Release_SubsequentRentReturnsNull(bool multiple) public void RentReturnRelease_SubsequentRentReturnsDifferentHandles(bool multiple)
{ {
var pool = new UniformUnmanagedMemoryPool(16, 16); 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(); 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) if (multiple)
{ {
UnmanagedMemoryHandle b = pool.Rent(); UnmanagedMemoryHandle b = pool.Rent();
Assert.Null(b); cleanup.Register(b);
Assert.NotEqual(h0, b.Handle);
Assert.NotEqual(h1, b.Handle);
} }
else else
{ {
UnmanagedMemoryHandle[] b = pool.Rent(2); UnmanagedMemoryHandle[] b = pool.Rent(2);
Assert.Null(b); cleanup.Register(b);
} Assert.NotEqual(h0, b[0].Handle);
} Assert.NotEqual(h1, b[0].Handle);
Assert.NotEqual(h0, b[1].Handle);
[Theory] Assert.NotEqual(h1, b[1].Handle);
[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);
} }
} }
@ -257,6 +302,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
int count = Environment.ProcessorCount * 200; int count = Environment.ProcessorCount * 200;
var pool = new UniformUnmanagedMemoryPool(8, count); var pool = new UniformUnmanagedMemoryPool(8, count);
using var cleanup = new CleanupUtil(pool);
var rnd = new Random(0); var rnd = new Random(0);
Parallel.For(0, Environment.ProcessorCount, (int i) => Parallel.For(0, Environment.ProcessorCount, (int i) =>
@ -267,8 +313,8 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
UnmanagedMemoryHandle[] data = pool.Rent(2); UnmanagedMemoryHandle[] data = pool.Rent(2);
GetSpan(pool, data[0]).Fill((byte)i); data[0].GetSpan().Fill((byte)i);
GetSpan(pool, data[1]).Fill((byte)i); data[1].GetSpan().Fill((byte)i);
allArrays.Add(data[0]); allArrays.Add(data[0]);
allArrays.Add(data[1]); allArrays.Add(data[1]);
@ -283,7 +329,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
foreach (UnmanagedMemoryHandle array in allArrays) foreach (UnmanagedMemoryHandle array in allArrays)
{ {
Assert.True(expected.SequenceEqual(GetSpan(pool, array))); Assert.True(expected.SequenceEqual(array.GetSpan()));
pool.Return(new[] { array }); 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); Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles);
b.Dispose(); b.Dispose();
g.Dispose(); g.Dispose();
Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles);
allocator.ReleaseRetainedResources();
Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles);
} }
} }
@ -307,7 +309,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
[ConditionalTheory(nameof(IsWindows))] [ConditionalTheory(nameof(IsWindows))]
[InlineData(300)] [InlineData(300)]
[InlineData(600)] [InlineData(600)]
[InlineData(1200)]
public void MemoryOwnerFinalizer_ReturnsToPool(int length) public void MemoryOwnerFinalizer_ReturnsToPool(int length)
{ {
// RunTest(length.ToString()); // RunTest(length.ToString());
@ -332,9 +333,11 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
using IMemoryOwner<byte> g = allocator.Allocate<byte>(lengthInner); using IMemoryOwner<byte> g = allocator.Allocate<byte>(lengthInner);
Assert.Equal(42, g.GetSpan()[0]); Assert.Equal(42, g.GetSpan()[0]);
GC.KeepAlive(allocator);
} }
} }
[MethodImpl(MethodImplOptions.NoInlining)]
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false) private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
{ {
IMemoryOwner<byte> g = allocator.Allocate<byte>(length); 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] [Fact]
public unsafe void Allocate_AllocatesReadWriteMemory() public unsafe void Allocate_AllocatesReadWriteMemory()
{ {
using var h = UnmanagedMemoryHandle.Allocate(128); var h = UnmanagedMemoryHandle.Allocate(128);
Assert.False(h.IsClosed);
Assert.False(h.IsInvalid); 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++) for (int i = 0; i < 128; i++)
{ {
ptr[i] = (byte)i; ptr[i] = (byte)i;
@ -27,21 +27,23 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
{ {
Assert.Equal((byte)i, ptr[i]); Assert.Equal((byte)i, ptr[i]);
} }
h.Free();
} }
[Fact] [Fact]
public void Dispose_ClosesHandle() public void Free_ClosesHandle()
{ {
var h = UnmanagedMemoryHandle.Allocate(128); var h = UnmanagedMemoryHandle.Allocate(128);
h.Dispose(); h.Free();
Assert.True(h.IsClosed);
Assert.True(h.IsInvalid); Assert.True(h.IsInvalid);
Assert.Equal(IntPtr.Zero, h.Handle);
} }
[Theory] [Theory]
[InlineData(1)] [InlineData(1)]
[InlineData(13)] [InlineData(13)]
public void CreateDispose_TracksAllocations(int count) public void Create_Free_AllocationsAreTracked(int count)
{ {
RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose(); RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose();
@ -60,125 +62,39 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators
for (int i = 0; i < countInner; i++) for (int i = 0; i < countInner; i++)
{ {
Assert.Equal(countInner - i, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.Equal(countInner - i, UnmanagedMemoryHandle.TotalOutstandingHandles);
l[i].Dispose(); l[i].Free();
Assert.Equal(countInner - i - 1, UnmanagedMemoryHandle.TotalOutstandingHandles); 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] [Fact]
public void Resurrect_PreventsFinalization() public void Equality_WhenTrue()
{ {
RemoteExecutor.Invoke(RunTest).Dispose(); var h1 = UnmanagedMemoryHandle.Allocate(10);
UnmanagedMemoryHandle h2 = h1;
static void RunTest()
{ Assert.True(h1.Equals(h2));
AllocateResurrect(); Assert.True(h2.Equals(h1));
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.True(h1 == h2);
GC.Collect(); Assert.False(h1 != h2);
GC.WaitForPendingFinalizers(); Assert.True(h1.GetHashCode() == h2.GetHashCode());
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); h1.Free();
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();
}
} }
[Fact] [Fact]
public void AssignedToNewOwner_ReRegistersForFinalization() public void Equality_WhenFalse()
{ {
RemoteExecutor.Invoke(RunTest).Dispose(); var h1 = UnmanagedMemoryHandle.Allocate(10);
var h2 = UnmanagedMemoryHandle.Allocate(10);
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));
}
static void VerifyResurrectedHandle(bool reAssign) Assert.False(h1.Equals(h2));
{ Assert.False(h2.Equals(h1));
Assert.NotNull(resurrectedHandle); Assert.False(h1 == h2);
Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); Assert.True(h1 != h2);
Assert.False(resurrectedHandle.IsClosed);
Assert.False(resurrectedHandle.IsInvalid);
resurrectedHandle.AssignedToNewOwner();
if (reAssign)
{
_ = new HandleOwner(resurrectedHandle);
}
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 public class Allocate : MemoryGroupTestsBase
{ {
#pragma warning disable SA1509 #pragma warning disable SA1509
public static TheoryData<object, int, int, long, int, int, int> AllocateData = public static TheoryData<object, int, int, long, int, int, int> AllocateData = new()
new TheoryData<object, int, int, long, int, int, int>()
{ {
{ default(S5), 22, 4, 4, 1, 4, 4 }, { default(S5), 22, 4, 4, 1, 4, 4 },
{ default(S5), 22, 4, 7, 2, 4, 3 }, { default(S5), 22, 4, 7, 2, 4, 3 },
@ -100,9 +99,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
g.Dispose(); g.Dispose();
} }
private static unsafe Span<byte> GetSpan(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) =>
new Span<byte>((void*)h.DangerousGetHandle(), pool.BufferLength);
[Theory] [Theory]
[InlineData(AllocationOptions.None)] [InlineData(AllocationOptions.None)]
[InlineData(AllocationOptions.Clean)] [InlineData(AllocationOptions.Clean)]
@ -112,7 +108,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
UnmanagedMemoryHandle[] buffers = pool.Rent(5); UnmanagedMemoryHandle[] buffers = pool.Rent(5);
foreach (UnmanagedMemoryHandle b in buffers) foreach (UnmanagedMemoryHandle b in buffers)
{ {
GetSpan(pool, b).Fill(42); b.GetSpan().Fill(42);
} }
pool.Return(buffers); pool.Return(buffers);

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

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

Loading…
Cancel
Save