Browse Source

Merge 214882f157 into 936a65bdbf

pull/3120/merge
James Jackson-South 5 days ago
committed by GitHub
parent
commit
ed9d8e0671
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 61
      src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
  2. 41
      src/ImageSharp/Memory/AllocationTrackingState.cs
  3. 14
      src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
  4. 2
      src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
  5. 2
      src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
  6. 4
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  7. 4
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
  8. 218
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  9. 30
      src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
  10. 37
      src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
  11. 24
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  12. 13
      src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
  13. 8
      src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
  14. 16
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
  15. 5
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  16. 35
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  17. 4
      tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
  18. 44
      tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
  19. 56
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  20. 10
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
  21. 6
      tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs

61
src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs

@ -0,0 +1,61 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp.Memory;
/// <summary>
/// Provides the tracked memory-owner contract required by <see cref="MemoryAllocator"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <remarks>
/// Custom allocators implement <see cref="MemoryAllocator.AllocateCore{T}(int, AllocationOptions)"/>
/// and return a derived type. The base allocator attaches allocation tracking after the owner has been
/// created so custom implementations cannot forget, duplicate, or mismatch the reservation lifecycle.
/// </remarks>
public abstract class AllocationTrackedMemoryManager<T> : MemoryManager<T>
where T : struct
{
private AllocationTrackingState allocationTracking;
/// <summary>
/// Releases resources held by the concrete tracked owner.
/// </summary>
/// <param name="disposing">
/// <see langword="true"/> when the owner is being disposed deterministically;
/// otherwise, <see langword="false"/>.
/// </param>
/// <remarks>
/// Implementations release their own resources here. Allocation tracking is released by the sealed base
/// dispose path after this method returns.
/// </remarks>
protected abstract void DisposeCore(bool disposing);
/// <inheritdoc />
protected sealed override void Dispose(bool disposing)
{
this.DisposeCore(disposing);
this.ReleaseAllocationTracking();
}
/// <summary>
/// Attaches allocation tracking to this owner after allocation has succeeded.
/// </summary>
/// <param name="allocator">The allocator that owns the reservation for this instance.</param>
/// <param name="lengthInBytes">The reserved allocation size, in bytes.</param>
/// <remarks>
/// <see cref="MemoryAllocator"/> calls this exactly once after <c>AllocateCore</c> returns.
/// Derived allocators should not call it themselves; they only construct the concrete owner.
/// </remarks>
internal void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
=> this.allocationTracking.Attach(allocator, lengthInBytes);
/// <summary>
/// Releases any tracked allocation bytes associated with this instance.
/// </summary>
/// <remarks>
/// Calling this more than once is safe; only the first call after tracking has been attached releases bytes.
/// </remarks>
private void ReleaseAllocationTracking() => this.allocationTracking.Release();
}

41
src/ImageSharp/Memory/AllocationTrackingState.cs

@ -0,0 +1,41 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Memory;
/// <summary>
/// Tracks a single allocator reservation and releases it exactly once.
/// </summary>
/// <remarks>
/// This type is intended to live as a mutable field on the owning object. It should not be copied
/// after tracking has been attached, because the owner relies on a single shared release state.
/// </remarks>
internal struct AllocationTrackingState
{
private MemoryAllocator? allocator;
private long lengthInBytes;
private int released;
/// <summary>
/// Attaches allocator reservation tracking to the current owner.
/// </summary>
/// <param name="allocator">The allocator that owns the reservation.</param>
/// <param name="lengthInBytes">The reserved allocation size, in bytes.</param>
internal void Attach(MemoryAllocator allocator, long lengthInBytes)
{
this.allocator = allocator;
this.lengthInBytes = lengthInBytes;
}
/// <summary>
/// Releases the attached allocator reservation once.
/// </summary>
internal void Release()
{
if (Interlocked.Exchange(ref this.released, 1) == 0 && this.allocator != null)
{
this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes);
this.allocator = null;
}
}
}

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

@ -1,9 +1,19 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Memory;
/// <summary>
/// Provides helper methods for working with <see cref="AllocationOptions"/>.
/// </summary>
internal static class AllocationOptionsExtensions
{
public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag;
/// <summary>
/// Returns a value indicating whether the specified flag is set on the allocation options.
/// </summary>
/// <param name="options">The allocation options to inspect.</param>
/// <param name="flag">The flag to test for.</param>
/// <returns><see langword="true"/> if <paramref name="flag"/> is set; otherwise, <see langword="false"/>.</returns>
public static bool Has(this AllocationOptions options, AllocationOptions flag)
=> (options & flag) == flag;
}

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

@ -47,7 +47,7 @@ internal class BasicArrayBuffer<T> : ManagedBufferBase<T>
public override Span<T> GetSpan() => this.Array.AsSpan(0, this.Length);
/// <inheritdoc />
protected override void Dispose(bool disposing)
protected override void DisposeCore(bool disposing)
{
}

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

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
/// Provides a base class for <see cref="IMemoryOwner{T}"/> implementations by implementing pinning logic for <see cref="MemoryManager{T}"/> adaption.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
internal abstract class ManagedBufferBase<T> : MemoryManager<T>
internal abstract class ManagedBufferBase<T> : AllocationTrackedMemoryManager<T>
where T : struct
{
private GCHandle pinHandle;

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

@ -13,7 +13,7 @@ internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>, IRefCounted
where T : struct
{
private readonly int lengthInBytes;
private LifetimeGuard lifetimeGuard;
private readonly LifetimeGuard lifetimeGuard;
public SharedArrayPoolBuffer(int lengthInElements)
{
@ -24,7 +24,7 @@ internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>, IRefCounted
public byte[]? Array { get; private set; }
protected override void Dispose(bool disposing)
protected override void DisposeCore(bool disposing)
{
if (this.Array == null)
{

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

@ -12,7 +12,7 @@ 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 sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T>, IRefCounted
internal sealed unsafe class UnmanagedBuffer<T> : AllocationTrackedMemoryManager<T>, IRefCounted
where T : struct
{
private readonly int lengthInElements;
@ -52,7 +52,7 @@ internal sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T>, IRefCounted
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
protected override void DisposeCore(bool disposing)
{
DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!");

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

@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Memory;
public abstract class MemoryAllocator
{
private const int OneGigabyte = 1 << 30;
private long accumulativeAllocatedBytes;
private int trackingSuppressionCount;
/// <summary>
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
@ -23,9 +25,43 @@ public abstract class MemoryAllocator
/// </summary>
public static MemoryAllocator Default { get; } = Create();
internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
/// <summary>
/// Gets the maximum number of bytes that can be allocated by a memory group.
/// </summary>
/// <remarks>
/// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and
/// 1 GB for 32-bit processes.
/// </remarks>
internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
/// <summary>
/// Gets the maximum allowed total allocation size, in bytes, for the current process.
/// </summary>
/// <remarks>
/// Defaults to <see cref="long.MaxValue"/>, effectively imposing no limit on total allocations.
/// This property can be set to enforce a cap on total memory usage across all allocations made through this allocator instance, providing
/// a safeguard against excessive memory consumption.<br/>
/// When the cumulative size of active allocations exceeds this limit, an <see cref="InvalidMemoryOperationException"/> will be thrown to
/// prevent further allocations and signal that the limit has been breached.
/// </remarks>
internal long AccumulativeAllocationLimitBytes { get; private protected set; } = long.MaxValue;
/// <summary>
/// Gets the maximum size, in bytes, that can be allocated for a single buffer.
/// </summary>
/// <remarks>
/// The single buffer allocation limit is set to 1 GB by default.
/// </remarks>
internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte;
/// <summary>
/// Gets a value indicating whether accumulative allocation tracking is currently suppressed for this instance.
/// </summary>
/// <remarks>
/// This is used internally when an outer allocator or memory group reservation already owns the tracked bytes
/// and nested allocations must not reserve or release them a second time.
/// </remarks>
private bool IsTrackingSuppressed => Volatile.Read(ref this.trackingSuppressionCount) > 0;
/// <summary>
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@ -53,6 +89,11 @@ public abstract class MemoryAllocator
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
}
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
allocator.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
return allocator;
}
@ -63,15 +104,72 @@ public abstract class MemoryAllocator
/// <param name="length">Size of the buffer to allocate.</param>
/// <param name="options">The allocation options.</param>
/// <returns>A buffer of values of type <typeparamref name="T"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">When length is zero or negative.</exception>
/// <exception cref="ArgumentOutOfRangeException">When length is negative.</exception>
/// <exception cref="InvalidMemoryOperationException">When length is over the capacity of the allocator.</exception>
public abstract IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
public IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
{
if (length < 0)
{
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
}
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
{
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
}
long lengthInBytesLong = (long)lengthInBytes;
bool shouldTrack = !this.IsTrackingSuppressed && lengthInBytesLong != 0;
if (shouldTrack)
{
this.ReserveAllocation(lengthInBytesLong);
}
try
{
AllocationTrackedMemoryManager<T> owner = this.AllocateCore<T>(length, options);
if (shouldTrack)
{
owner.AttachAllocationTracking(this, lengthInBytesLong);
}
return owner;
}
catch
{
if (shouldTrack)
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
}
throw;
}
}
/// <summary>
/// Allocates a tracked memory owner for <see cref="Allocate{T}(int, AllocationOptions)"/>.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="length">Size of the buffer to allocate.</param>
/// <param name="options">The allocation options.</param>
/// <returns>A tracked memory owner of values of type <typeparamref name="T"/>.</returns>
/// <remarks>
/// Implementations should only allocate and initialize the concrete owner. The base allocator
/// reserves bytes, attaches tracking to the returned owner, and releases the reservation if allocation fails.
/// </remarks>
protected abstract AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct;
/// <summary>
/// Releases all retained resources not being in use.
/// Eg: by resetting array pools and letting GC to free the arrays.
/// </summary>
/// <remarks>
/// This does not dispose active allocations; callers are responsible for disposing all
/// <see cref="IMemoryOwner{T}"/> instances to release memory.
/// </remarks>
public virtual void ReleaseRetainedResources()
{
}
@ -102,11 +200,119 @@ public abstract class MemoryAllocator
InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes);
}
// Cast to long is safe because we already checked that the total length is within the limit.
return this.AllocateGroupCore<T>(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
long totalLengthInBytesLong = (long)totalLengthInBytes;
bool shouldTrack = !this.IsTrackingSuppressed && totalLengthInBytesLong != 0;
if (shouldTrack)
{
this.ReserveAllocation(totalLengthInBytesLong);
}
using (this.SuppressTracking())
{
try
{
MemoryGroup<T> group = this.AllocateGroupCore<T>(totalLength, totalLengthInBytesLong, bufferAlignment, options);
if (shouldTrack)
{
group.AttachAllocationTracking(this, totalLengthInBytesLong);
}
return group;
}
catch
{
if (shouldTrack)
{
this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
}
throw;
}
}
}
internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
where T : struct
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options);
/// <summary>
/// Allocates a single segment for <see cref="MemoryGroup{T}"/> construction.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="length">Size of the segment to allocate.</param>
/// <param name="options">The allocation options.</param>
/// <returns>A segment owner for the requested buffer length.</returns>
/// <remarks>
/// The default implementation uses <see cref="Allocate{T}(int, AllocationOptions)"/>. Built-in allocators
/// can override this to supply raw segment owners when group construction must bypass nested tracking.
/// </remarks>
internal virtual IMemoryOwner<T> AllocateGroupBuffer<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
=> this.Allocate<T>(length, options);
/// <summary>
/// Reserves accumulative allocation bytes before creating the underlying buffer.
/// </summary>
/// <param name="lengthInBytes">The number of bytes to reserve.</param>
private void ReserveAllocation(long lengthInBytes)
{
if (this.IsTrackingSuppressed || lengthInBytes <= 0)
{
return;
}
long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes);
if (total > this.AccumulativeAllocationLimitBytes)
{
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
InvalidMemoryOperationException.ThrowAllocationOverLimitException((ulong)lengthInBytes, this.AccumulativeAllocationLimitBytes);
}
}
/// <summary>
/// Releases accumulative allocation bytes previously tracked by this allocator.
/// </summary>
/// <param name="lengthInBytes">The number of bytes to release.</param>
internal void ReleaseAccumulatedBytes(long lengthInBytes)
{
if (lengthInBytes <= 0)
{
return;
}
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
}
/// <summary>
/// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
/// </summary>
/// <returns>A scope that restores tracking when disposed.</returns>
/// <remarks>
/// Returning the concrete scope type keeps nested allocator calls allocation-free on the hot path
/// while preserving the same using-pattern at call sites.
/// </remarks>
private TrackingSuppressionScope SuppressTracking() => new(this);
/// <summary>
/// Temporarily suppresses accumulative allocation tracking within a scope.
/// </summary>
private struct TrackingSuppressionScope : IDisposable
{
private MemoryAllocator? allocator;
public TrackingSuppressionScope(MemoryAllocator allocator)
{
this.allocator = allocator;
_ = Interlocked.Increment(ref allocator.trackingSuppressionCount);
}
public void Dispose()
{
if (this.allocator != null)
{
_ = Interlocked.Decrement(ref this.allocator.trackingSuppressionCount);
this.allocator = null;
}
}
}
}

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

@ -10,6 +10,7 @@ public struct MemoryAllocatorOptions
{
private int? maximumPoolSizeMegabytes;
private int? allocationLimitMegabytes;
private int? accumulativeAllocationLimitMegabytes;
/// <summary>
/// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool
@ -17,7 +18,7 @@ public struct MemoryAllocatorOptions
/// </summary>
public int? MaximumPoolSizeMegabytes
{
get => this.maximumPoolSizeMegabytes;
readonly get => this.maximumPoolSizeMegabytes;
set
{
if (value.HasValue)
@ -35,7 +36,7 @@ public struct MemoryAllocatorOptions
/// </summary>
public int? AllocationLimitMegabytes
{
get => this.allocationLimitMegabytes;
readonly get => this.allocationLimitMegabytes;
set
{
if (value.HasValue)
@ -46,4 +47,29 @@ public struct MemoryAllocatorOptions
this.allocationLimitMegabytes = value;
}
}
/// <summary>
/// Gets or sets a value defining the maximum total size that can be allocated by the allocator in Megabytes.
/// <see langword="null"/> means platform default: 2GB on 32-bit processes, 8GB on 64-bit processes.
/// </summary>
public int? AccumulativeAllocationLimitMegabytes
{
readonly get => this.accumulativeAllocationLimitMegabytes;
set
{
if (value.HasValue)
{
Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AccumulativeAllocationLimitMegabytes));
if (this.AllocationLimitMegabytes.HasValue)
{
Guard.MustBeGreaterThanOrEqualTo(
value.Value,
this.AllocationLimitMegabytes.Value,
nameof(this.AccumulativeAllocationLimitMegabytes));
}
}
this.accumulativeAllocationLimitMegabytes = value;
}
}
}

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

@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory;
@ -12,23 +10,36 @@ namespace SixLabors.ImageSharp.Memory;
/// </summary>
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
/// <inheritdoc />
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with default limits.
/// </summary>
public SimpleGcMemoryAllocator()
: this(default)
{
}
/// <inheritdoc />
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with custom limits.
/// </summary>
/// <param name="options">The <see cref="MemoryAllocatorOptions"/> to apply.</param>
public SimpleGcMemoryAllocator(MemoryAllocatorOptions options)
{
if (length < 0)
if (options.AllocationLimitMegabytes.HasValue)
{
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
}
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
return new BasicArrayBuffer<T>(new T[length]);
}
/// <inheritdoc />
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
/// <inheritdoc />
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
=> new BasicArrayBuffer<T>(new T[length]);
}

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
@ -75,22 +74,12 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes;
/// <inheritdoc />
public override IMemoryOwner<T> Allocate<T>(
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(
int length,
AllocationOptions options = AllocationOptions.None)
{
if (length < 0)
{
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
}
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
{
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
}
if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
int lengthInBytes = length * Unsafe.SizeOf<T>();
if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
SharedArrayPoolBuffer<T> buffer = new(length);
if (options.Has(AllocationOptions.Clean))
@ -101,17 +90,16 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
return buffer;
}
if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
if (lengthInBytes <= this.poolBufferSizeInBytes)
{
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
return buffer;
return this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
}
}
return this.nonPoolAllocator.Allocate<T>(length, options);
return UnmanagedMemoryAllocator.AllocateBuffer<T>(length, options);
}
/// <inheritdoc />

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

@ -18,7 +18,18 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
=> AllocateBuffer<T>(length, options);
internal override IMemoryOwner<T> AllocateGroupBuffer<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
=> AllocateBuffer<T>(length, options);
// The pooled allocator uses this internal entry point when it needs a raw unmanaged owner without
// nesting another allocator-level reservation cycle around the fallback allocation.
internal static UnmanagedBuffer<T> AllocateBuffer<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
{
UnmanagedBuffer<T> buffer = UnmanagedBuffer<T>.Allocate(length);
if (options.Has(AllocationOptions.Clean))

8
src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs

@ -15,12 +15,12 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller.
/// </summary>
int BufferLength { get; }
public int BufferLength { get; }
/// <summary>
/// Gets the aggregate number of elements in the group.
/// </summary>
long TotalLength { get; }
public long TotalLength { get; }
/// <summary>
/// Gets a value indicating whether the group has been invalidated.
@ -29,7 +29,7 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces
/// the image buffers internally.
/// </remarks>
bool IsValid { get; }
public bool IsValid { get; }
/// <summary>
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
@ -39,5 +39,5 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// implementation, which is still available when casting to one of the underlying interfaces.
/// </summary>
/// <returns>A new <see cref="MemoryGroupEnumerator{T}"/> instance mapping the current <see cref="Memory{T}"/> values in use.</returns>
new MemoryGroupEnumerator<T> GetEnumerator();
public new MemoryGroupEnumerator<T> GetEnumerator();
}

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

@ -31,23 +31,23 @@ internal abstract partial class MemoryGroup<T>
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}
public override MemoryGroupEnumerator<T> GetEnumerator() => new(this);
/// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
/* The runtime sees the Array class as if it implemented the
* type-generic collection interfaces explicitly, so here we
* can just cast the source array to IList<Memory<T>> (or to
* an equivalent type), and invoke the generic GetEnumerator
* method directly from that interface reference. This saves
* having to create our own iterator block here. */
return ((IList<Memory<T>>)this.source).GetEnumerator();
}
=> ((IList<Memory<T>>)this.source).GetEnumerator();
public override void Dispose() => this.View.Invalidate();
public override void Dispose()
{
this.View.Invalidate();
this.ReleaseAllocationTracking();
}
}
}

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

@ -73,8 +73,8 @@ internal abstract partial class MemoryGroup<T>
result[i] = currentBuffer;
}
ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options);
result[result.Length - 1] = lastBuffer;
ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[^1], sizeOfLastBuffer, options);
result[^1] = lastBuffer;
return result;
}
@ -155,6 +155,7 @@ internal abstract partial class MemoryGroup<T>
}
}
this.ReleaseAllocationTracking();
this.memoryOwners = null;
this.IsValid = false;
this.groupLifetimeGuard = null;

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

@ -21,6 +21,7 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
{
private static readonly int ElementSize = Unsafe.SizeOf<T>();
private AllocationTrackingState allocationTracking;
private MemoryGroupSpanCache memoryGroupSpanCache;
private MemoryGroup(int bufferLength, long totalLength)
@ -52,16 +53,36 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
/// <inheritdoc />
public abstract MemoryGroupEnumerator<T> GetEnumerator();
/// <summary>
/// Attaches allocation tracking by specifying the allocator and the length, in bytes, to be tracked.
/// </summary>
/// <param name="allocator">The memory allocator to use for tracking allocations.</param>
/// <param name="lengthInBytes">The length, in bytes, of the memory region to track. Must be greater than or equal to zero.</param>
/// <remarks>
/// Intended for one-time initialization after the group has been created; callers should avoid changing
/// tracking state concurrently with disposal.
/// </remarks>
internal void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) =>
this.allocationTracking.Attach(allocator, lengthInBytes);
/// <summary>
/// Releases any resources or tracking information associated with allocation tracking for this instance.
/// </summary>
/// <remarks>
/// This method is intended to be called when allocation tracking is no longer needed. It is safe
/// to call multiple times; subsequent calls after the first have no effect, even when called concurrently.
/// </remarks>
internal void ReleaseAllocationTracking() => this.allocationTracking.Release();
/// <inheritdoc />
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
/* This method is implemented in each derived class.
* Implementing the method here as non-abstract and throwing,
* then reimplementing it explicitly in each derived class, is
* a workaround for the lack of support for abstract explicit
* interface method implementations in C#. */
throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()");
}
=> throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()");
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();
@ -97,8 +118,8 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
if (totalLengthInElements == 0)
{
IMemoryOwner<T>[] buffers0 = [allocator.Allocate<T>(0, options)];
return new Owned(buffers0, 0, 0, true);
IMemoryOwner<T>[] emptyBuffer = [allocator.AllocateGroupBuffer<T>(0, options)];
return new Owned(emptyBuffer, 0, 0, true);
}
int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements;
@ -123,12 +144,12 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
IMemoryOwner<T>[] buffers = new IMemoryOwner<T>[bufferCount];
for (int i = 0; i < buffers.Length - 1; i++)
{
buffers[i] = allocator.Allocate<T>(bufferLength, options);
buffers[i] = allocator.AllocateGroupBuffer<T>(bufferLength, options);
}
if (bufferCount > 0)
{
buffers[^1] = allocator.Allocate<T>(sizeOfLastBuffer, options);
buffers[^1] = allocator.AllocateGroupBuffer<T>(sizeOfLastBuffer, options);
}
return new Owned(buffers, bufferLength, totalLengthInElements, true);

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

@ -313,7 +313,7 @@ public abstract class ProcessPixelRowsTestBase
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) =>
this.buffers.Pop() as IMemoryOwner<T>;
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None) =>
this.buffers.Pop() as AllocationTrackedMemoryManager<T>;
}
}

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

@ -48,6 +48,50 @@ public class SimpleGcMemoryAllocatorTests
}
}
[Fact]
public void Allocate_AccumulativeLimit_ReleasesOnOwnerDispose()
{
SimpleGcMemoryAllocator allocator = new(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single owner.
IMemoryOwner<byte> b0 = allocator.Allocate<byte>(oneMb);
// Additional allocation should exceed the limit while the owner is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1));
// Disposing the owner releases the reservation.
b0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.Allocate<byte>(oneMb).Dispose();
}
[Fact]
public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose()
{
SimpleGcMemoryAllocator allocator = new(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single group.
MemoryGroup<byte> g0 = allocator.AllocateGroup<byte>(oneMb, 1024);
// Additional allocation should exceed the limit while the group is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(1, 1024));
// Disposing the group releases the reservation.
g0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
}
[StructLayout(LayoutKind.Explicit, Size = 512)]
private struct BigStruct
{

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

@ -16,8 +16,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
{
public class BufferTests1 : BufferTestSuite
{
private static MemoryAllocator CreateMemoryAllocator() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(
private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() =>
new(
sharedArrayPoolThresholdInBytes: 1024,
poolBufferSizeInBytes: 2048,
maxPoolSizeInBytes: 2048 * 4,
@ -31,8 +31,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
public class BufferTests2 : BufferTestSuite
{
private static MemoryAllocator CreateMemoryAllocator() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(
private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() =>
new(
sharedArrayPoolThresholdInBytes: 512,
poolBufferSizeInBytes: 1024,
maxPoolSizeInBytes: 1024 * 4,
@ -179,8 +179,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
g1.Dispose();
// Do some unmanaged allocations to make sure new non-pooled unmanaged allocations will grab different memory:
IntPtr dummy1 = Marshal.AllocHGlobal((IntPtr)B(8));
IntPtr dummy2 = Marshal.AllocHGlobal((IntPtr)B(8));
IntPtr dummy1 = Marshal.AllocHGlobal(checked((IntPtr)B(8)));
IntPtr dummy2 = Marshal.AllocHGlobal(checked((IntPtr)B(8)));
using MemoryGroup<byte> g2 = allocator.AllocateGroup<byte>(B(8), 1024);
using MemoryGroup<byte> g3 = allocator.AllocateGroup<byte>(B(8), 1024);
@ -433,6 +433,50 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(5 * oneMb, 1024));
}
[Fact]
public void Allocate_AccumulativeLimit_ReleasesOnOwnerDispose()
{
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single owner.
IMemoryOwner<byte> b0 = allocator.Allocate<byte>(oneMb);
// Additional allocation should exceed the limit while the owner is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1));
// Disposing the owner releases the reservation.
b0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.Allocate<byte>(oneMb).Dispose();
}
[Fact]
public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose()
{
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single group.
MemoryGroup<byte> g0 = allocator.AllocateGroup<byte>(oneMb, 1024);
// Additional allocation should exceed the limit while the group is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(1, 1024));
// Disposing the group releases the reservation.
g0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
}
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
public void MemoryAllocator_Create_SetHighLimit()
{

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

@ -98,7 +98,15 @@ public partial class MemoryGroupTests
[InlineData(AllocationOptions.Clean)]
public unsafe void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options)
{
UniformUnmanagedMemoryPool pool = new(10, 5);
// Disable trimming to avoid buffers being freed between Return and TryAllocate by the
// trim timer or the Gen2 GC callback.
UniformUnmanagedMemoryPool pool = new(
10,
5,
new UniformUnmanagedMemoryPool.TrimSettings
{
Rate = 0
});
UnmanagedMemoryHandle[] buffers = pool.Rent(5);
foreach (UnmanagedMemoryHandle b in buffers)
{

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

@ -37,7 +37,7 @@ internal class TestMemoryAllocator : MemoryAllocator
this.returnLog = new List<ReturnRequest>();
}
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
{
T[] array = this.AllocateArray<T>(length, options);
return new BasicArrayBuffer<T>(array, length, this);
@ -110,7 +110,7 @@ internal class TestMemoryAllocator : MemoryAllocator
/// <summary>
/// Wraps an array as an <see cref="IManagedByteBuffer"/> instance.
/// </summary>
private class BasicArrayBuffer<T> : MemoryManager<T>
private class BasicArrayBuffer<T> : AllocationTrackedMemoryManager<T>
where T : struct
{
private readonly TestMemoryAllocator allocator;
@ -159,7 +159,7 @@ internal class TestMemoryAllocator : MemoryAllocator
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
protected override void DisposeCore(bool disposing)
{
if (disposing)
{

Loading…
Cancel
Save