Browse Source

Add AllocationTrackedMemoryManager and refactor allocators (#3120)

* Add AllocationTrackedMemoryManager and refactor allocators

* Add AllocationTrackingState and refactor tracking

* Cleanup

* Address feedback

* Introduce ApplyOptions and use in allocators

* Propagate allocation tracking to lifetime guards

* Address Copilot feedback

* Fix multi-buffer group tracking and enforce limits

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix override accesibility

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
pull/3133/head v4.0.0
James Jackson-South 1 month ago
committed by GitHub
parent
commit
ff36e83c74
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 67
      src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
  2. 47
      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. 13
      src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
  7. 7
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  8. 7
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
  9. 202
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  10. 38
      src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
  11. 33
      src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
  12. 33
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  13. 9
      src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
  14. 8
      src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
  15. 16
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
  16. 57
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  17. 37
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  18. 5
      src/ImageSharp/Memory/InvalidMemoryOperationException.cs
  19. 12
      tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
  20. 44
      tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
  21. 167
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  22. 10
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
  23. 6
      tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs

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

@ -0,0 +1,67 @@
// 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)
{
try
{
this.DisposeCore(disposing);
}
finally
{
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>
protected internal virtual 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();
}

47
src/ImageSharp/Memory/AllocationTrackingState.cs

@ -0,0 +1,47 @@
// 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>
/// <remarks>
/// Must complete-before the owning object's reference is observable to any other thread.
/// <see cref="MemoryAllocator"/> guarantees this by attaching synchronously on the allocating
/// thread before returning the owner; reference publication then provides the release fence
/// that makes these field writes visible to a subsequent <see cref="Release"/> on another thread.
/// </remarks>
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;

13
src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs

@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
/// </summary>
internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
{
private AllocationTrackingState allocationTracking;
private int refCount = 1;
private int disposed;
private int released;
@ -38,6 +39,14 @@ internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
public void ReleaseRef() => this.ReleaseRef(false);
/// <summary>
/// Attaches allocator reservation tracking to this lifetime guard.
/// </summary>
/// <param name="allocator">The allocator that owns the reservation.</param>
/// <param name="lengthInBytes">The reserved allocation size, in bytes.</param>
public void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
=> this.allocationTracking.Attach(allocator, lengthInBytes);
public void Dispose()
{
int wasDisposed = Interlocked.Exchange(ref this.disposed, 1);
@ -69,6 +78,10 @@ internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
}
this.Release();
// Guard-backed resources can be recovered by finalization, so their allocator
// reservation must follow the guard's actual release point instead of the owner object.
this.allocationTracking.Release();
}
}
}

7
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,10 @@ internal class SharedArrayPoolBuffer<T> : ManagedBufferBase<T>, IRefCounted
public byte[]? Array { get; private set; }
protected override void Dispose(bool disposing)
protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
=> this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
protected override void DisposeCore(bool disposing)
{
if (this.Array == null)
{

7
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;
@ -31,6 +31,9 @@ internal sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T>, IRefCounted
public void* Pointer => this.lifetimeGuard.Handle.Pointer;
protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
=> this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
public override Span<T> GetSpan()
{
DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name);
@ -52,7 +55,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!");

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

@ -12,6 +12,7 @@ namespace SixLabors.ImageSharp.Memory;
public abstract class MemoryAllocator
{
private const int OneGigabyte = 1 << 30;
private long accumulativeAllocatedBytes;
/// <summary>
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
@ -23,9 +24,34 @@ 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 accumulative size, in bytes, of all active allocations made through this allocator instance.
/// </summary>
/// <remarks>
/// Defaults to <see cref="long.MaxValue"/>, effectively imposing no limit on the accumulative total.
/// When set, this provides a safeguard against excessive memory consumption by capping the combined size of
/// outstanding allocations issued by this instance.<br/>
/// When the accumulative 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 the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@ -47,13 +73,26 @@ public abstract class MemoryAllocator
public static MemoryAllocator Create(MemoryAllocatorOptions options)
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
allocator.ApplyOptions(options);
return allocator;
}
/// <summary>
/// Applies the supplied <see cref="MemoryAllocatorOptions"/> to this instance.
/// </summary>
/// <param name="options">The options to apply. Properties left as <see langword="null"/> are ignored.</param>
private protected void ApplyOptions(MemoryAllocatorOptions options)
{
if (options.AllocationLimitMegabytes.HasValue)
{
allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
}
return allocator;
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
}
/// <summary>
@ -63,15 +102,60 @@ 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="InvalidMemoryOperationException">When length is over the capacity of the allocator.</exception>
public abstract IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
/// <exception cref="InvalidMemoryOperationException">When length is negative or over the capacity of the allocator.</exception>
public IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
{
long lengthInBytes = this.GetValidatedAllocationLengthInBytes<T>(length);
bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && lengthInBytes != 0;
if (shouldTrack)
{
this.ReserveAllocation(lengthInBytes);
}
try
{
AllocationTrackedMemoryManager<T> owner = this.AllocateCore<T>(length, options);
if (shouldTrack)
{
owner.AttachAllocationTracking(this, lengthInBytes);
}
return owner;
}
catch
{
if (shouldTrack)
{
this.ReleaseAccumulatedBytes(lengthInBytes);
}
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 +186,109 @@ 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.AccumulativeAllocationLimitBytes != long.MaxValue && totalLengthInBytesLong != 0;
if (shouldTrack)
{
this.ReserveAllocation(totalLengthInBytesLong);
}
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 validates the segment size then calls <see cref="AllocateCore{T}(int, AllocationOptions)"/>
/// directly so group construction can reserve and release the total allocation once.
/// </remarks>
internal virtual IMemoryOwner<T> AllocateGroupBuffer<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
{
_ = this.GetValidatedAllocationLengthInBytes<T>(length);
return this.AllocateCore<T>(length, options);
}
/// <summary>
/// Returns the validated allocation length in bytes.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="length">Size of the buffer to allocate.</param>
/// <returns>The allocation length in bytes.</returns>
private long GetValidatedAllocationLengthInBytes<T>(int length)
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);
}
return (long)lengthInBytes;
}
/// <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 (lengthInBytes <= 0)
{
return;
}
long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes);
if (total > this.AccumulativeAllocationLimitBytes)
{
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
InvalidMemoryOperationException.ThrowAccumulativeAllocationOverLimitException(lengthInBytes, total, 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);
}
}

38
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,15 +36,48 @@ public struct MemoryAllocatorOptions
/// </summary>
public int? AllocationLimitMegabytes
{
get => this.allocationLimitMegabytes;
readonly get => this.allocationLimitMegabytes;
set
{
if (value.HasValue)
{
Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes));
if (this.AccumulativeAllocationLimitMegabytes.HasValue)
{
Guard.MustBeLessThanOrEqualTo(
value.Value,
this.AccumulativeAllocationLimitMegabytes.Value,
nameof(this.AllocationLimitMegabytes));
}
}
this.allocationLimitMegabytes = value;
}
}
/// <summary>
/// Gets or sets a value defining the maximum accumulative size, in Megabytes, of all active allocations made
/// through the created <see cref="MemoryAllocator"/> instance.
/// <see langword="null"/> (the default) imposes no limit on the accumulative total.
/// </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;
}
}
}

33
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,24 @@ namespace SixLabors.ImageSharp.Memory;
/// </summary>
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with default limits.
/// </summary>
public SimpleGcMemoryAllocator()
: this(default)
{
}
/// <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) => this.ApplyOptions(options);
/// <inheritdoc />
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
/// <inheritdoc />
public override IMemoryOwner<T> Allocate<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);
}
return new BasicArrayBuffer<T>(new T[length]);
}
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
=> new BasicArrayBuffer<T>(new T[length]);
}

33
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;
@ -71,26 +70,25 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes);
}
internal UniformUnmanagedMemoryPoolMemoryAllocator(
int sharedArrayPoolThresholdInBytes,
int poolBufferSizeInBytes,
long maxPoolSizeInBytes,
int unmanagedBufferSizeInBytes,
MemoryAllocatorOptions options)
: this(sharedArrayPoolThresholdInBytes, poolBufferSizeInBytes, maxPoolSizeInBytes, unmanagedBufferSizeInBytes)
=> this.ApplyOptions(options);
/// <inheritdoc />
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 +99,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 />

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

@ -18,7 +18,14 @@ 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);
// 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();
}
}
}

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

@ -60,6 +60,58 @@ internal abstract partial class MemoryGroup<T>
}
}
internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
{
if (this.groupLifetimeGuard != null)
{
// Pool-owned multi-buffer groups recover leaked handles through the group guard finalizer.
this.groupLifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
return;
}
IMemoryOwner<T>[]? memoryOwners = this.memoryOwners;
if (memoryOwners?.Length == 1 && memoryOwners[0] is AllocationTrackedMemoryManager<T> trackedOwner)
{
// Single-buffer groups should release tracking with the buffer owner when that owner has
// a more precise lifetime, such as an existing pooled-resource finalizer.
trackedOwner.AttachAllocationTracking(allocator, lengthInBytes);
return;
}
if (memoryOwners?.Length > 1)
{
foreach (IMemoryOwner<T> memoryOwner in memoryOwners)
{
if (memoryOwner is not AllocationTrackedMemoryManager<T>)
{
// Splitting is only valid when every segment can own its reservation. A single
// untracked segment makes the whole group ineligible, and this preflight has
// not attached anything yet, so the entire group can fall back immediately.
base.AttachAllocationTracking(allocator, lengthInBytes);
return;
}
}
// Non-pool multi-buffer groups have no group-level finalizer, so each segment carries
// its own share of the reservation through the segment owner or its lifetime guard.
long remainingLengthInBytes = lengthInBytes;
int lastOwnerIndex = memoryOwners.Length - 1;
for (int i = 0; i < lastOwnerIndex; i++)
{
trackedOwner = (AllocationTrackedMemoryManager<T>)memoryOwners[i];
long ownerLengthInBytes = (long)trackedOwner.Memory.Length * Unsafe.SizeOf<T>();
trackedOwner.AttachAllocationTracking(allocator, ownerLengthInBytes);
remainingLengthInBytes -= ownerLengthInBytes;
}
trackedOwner = (AllocationTrackedMemoryManager<T>)memoryOwners[lastOwnerIndex];
trackedOwner.AttachAllocationTracking(allocator, remainingLengthInBytes);
return;
}
base.AttachAllocationTracking(allocator, lengthInBytes);
}
private static IMemoryOwner<T>[] CreateBuffers(
UnmanagedMemoryHandle[] pooledBuffers,
int bufferLength,
@ -73,8 +125,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 +207,7 @@ internal abstract partial class MemoryGroup<T>
}
}
this.ReleaseAllocationTracking();
this.memoryOwners = null;
this.IsValid = false;
this.groupLifetimeGuard = null;

37
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 virtual 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();
@ -81,8 +102,8 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
int bufferAlignmentInElements,
AllocationOptions options = AllocationOptions.None)
{
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
Guard.NotNull(allocator, nameof(allocator));
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
if (totalLengthInElements < 0)
{
@ -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);

5
src/ImageSharp/Memory/InvalidMemoryOperationException.cs

@ -39,4 +39,9 @@ public class InvalidMemoryOperationException : InvalidOperationException
[DoesNotReturn]
internal static void ThrowAllocationOverLimitException(ulong length, long limit) =>
throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={length} that exceeded the limit {limit}.");
[DoesNotReturn]
internal static void ThrowAccumulativeAllocationOverLimitException(long requestedLength, long totalLength, long limit) =>
throw new InvalidMemoryOperationException(
$"Attempted to allocate a buffer of length={requestedLength} that would increase the accumulative allocation size to {totalLength}, exceeding the limit {limit}.");
}

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

@ -313,7 +313,15 @@ 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)
{
object buffer = this.buffers.Pop();
if (buffer is AllocationTrackedMemoryManager<T> trackedBuffer)
{
return trackedBuffer;
}
throw new InvalidMemoryOperationException("The requested buffer type does not match the mock allocator buffer type.");
}
}
}

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
{

167
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);
@ -323,7 +323,7 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length)
private static void AllocateGroupAndForget(MemoryAllocator allocator, int length)
{
// Allocate a group and drop the reference without disposing.
// The test relies on the group's finalizer to return the rented memory to the pool.
@ -387,8 +387,34 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
}
}
[Theory]
[InlineData(1)] // SharedArrayPoolBuffer<T>
[InlineData(2)] // UniformUnmanagedMemoryPool buffer
public void Allocate_AccumulativeLimit_Finalization_ReleasesOwnerReservation(int megabytes)
{
RemoteExecutor.Invoke(RunTest, megabytes.ToString(CultureInfo.InvariantCulture)).Dispose();
static void RunTest(string megabytesStr)
{
int megabytesInner = int.Parse(megabytesStr, CultureInfo.InvariantCulture);
int length = megabytesInner * (1 << 20);
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = megabytesInner
});
AllocateSingleAndForget(allocator, length);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
allocator.Allocate<byte>(length).Dispose();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length)
private static void AllocateSingleAndForget(MemoryAllocator allocator, int length)
{
// Allocate and intentionally do not dispose.
IMemoryOwner<byte> g = allocator.Allocate<byte>(length);
@ -409,6 +435,30 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
g = null;
}
[Fact]
public void AllocateGroup_AccumulativeLimit_Finalization_ReleasesGroupReservation()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
const int megabytes = 5;
int length = megabytes * (1 << 20);
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = megabytes
});
AllocateGroupAndForget(allocator, length);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
allocator.AllocateGroup<byte>(length, 1024).Dispose();
}
}
[Fact]
public void Allocate_OverLimit_ThrowsInvalidMemoryOperationException()
{
@ -433,6 +483,107 @@ 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();
}
[Fact]
public void AllocateGroup_AccumulativeLimit_NonPoolFallback_TracksOncePerGroup()
{
// Configure the pool with zero capacity so multi-segment requests bypass both the
// single-buffer-from-pool path and MemoryGroup<T>.TryAllocate(pool, ...) and fall
// through to MemoryGroup<T>.Allocate(nonPoolAllocator, ...). The unmanaged segment
// size is small enough that the request must span multiple segments, which is the
// path where per-segment double-counting could regress.
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(
sharedArrayPoolThresholdInBytes: 64 * 1024,
poolBufferSizeInBytes: 128 * 1024,
maxPoolSizeInBytes: 0,
unmanagedBufferSizeInBytes: 256 * 1024,
new MemoryAllocatorOptions { AccumulativeAllocationLimitMegabytes = 1 });
// 768 KB exceeds the pool buffer size, so the request takes the multi-segment
// non-pool fallback (three 256 KB segments). If tracking double-counted (group
// plus each segment), reservation would be 768 KB + 768 KB = 1.5 MB and exceed
// the 1 MB limit on allocation itself.
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(768 * 1024, 1024);
Assert.True(g.Count > 1, "Test setup must exercise the multi-segment fallback path.");
// Reservation should be exactly 768 KB; another 512 KB would push to 1.25 MB and throw.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(512 * 1024));
g.Dispose();
// After disposal the reservation is fully released; a second equivalent group succeeds.
allocator.AllocateGroup<byte>(768 * 1024, 1024).Dispose();
}
[Fact]
public void AllocateGroup_AccumulativeLimit_NonPoolFallback_Finalization_ReleasesGroupReservation()
{
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(
sharedArrayPoolThresholdInBytes: 64 * 1024,
poolBufferSizeInBytes: 128 * 1024,
maxPoolSizeInBytes: 0,
unmanagedBufferSizeInBytes: 256 * 1024,
new MemoryAllocatorOptions { AccumulativeAllocationLimitMegabytes = 1 });
// This exercises the non-pool multi-segment fallback, where reservation ownership has
// to follow the finalizable segment guards because the MemoryGroup itself has no finalizer.
AllocateGroupAndForget(allocator, 768 * 1024);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
allocator.AllocateGroup<byte>(768 * 1024, 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