diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
new file mode 100644
index 0000000000..764bb37e82
--- /dev/null
+++ b/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;
+
+///
+/// Provides the tracked memory-owner contract required by .
+///
+/// The element type.
+///
+/// Custom allocators implement
+/// 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.
+///
+public abstract class AllocationTrackedMemoryManager : MemoryManager
+ where T : struct
+{
+ private AllocationTrackingState allocationTracking;
+
+ ///
+ /// Releases resources held by the concrete tracked owner.
+ ///
+ ///
+ /// when the owner is being disposed deterministically;
+ /// otherwise, .
+ ///
+ ///
+ /// Implementations release their own resources here. Allocation tracking is released by the sealed base
+ /// dispose path after this method returns.
+ ///
+ protected abstract void DisposeCore(bool disposing);
+
+ ///
+ protected sealed override void Dispose(bool disposing)
+ {
+ try
+ {
+ this.DisposeCore(disposing);
+ }
+ finally
+ {
+ this.ReleaseAllocationTracking();
+ }
+ }
+
+ ///
+ /// Attaches allocation tracking to this owner after allocation has succeeded.
+ ///
+ /// The allocator that owns the reservation for this instance.
+ /// The reserved allocation size, in bytes.
+ ///
+ /// calls this exactly once after AllocateCore returns.
+ /// Derived allocators should not call it themselves; they only construct the concrete owner.
+ ///
+ protected internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ => this.allocationTracking.Attach(allocator, lengthInBytes);
+
+ ///
+ /// Releases any tracked allocation bytes associated with this instance.
+ ///
+ ///
+ /// Calling this more than once is safe; only the first call after tracking has been attached releases bytes.
+ ///
+ private void ReleaseAllocationTracking() => this.allocationTracking.Release();
+}
diff --git a/src/ImageSharp/Memory/AllocationTrackingState.cs b/src/ImageSharp/Memory/AllocationTrackingState.cs
new file mode 100644
index 0000000000..1e9a632ed8
--- /dev/null
+++ b/src/ImageSharp/Memory/AllocationTrackingState.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Memory;
+
+///
+/// Tracks a single allocator reservation and releases it exactly once.
+///
+///
+/// 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.
+///
+internal struct AllocationTrackingState
+{
+ private MemoryAllocator? allocator;
+ private long lengthInBytes;
+ private int released;
+
+ ///
+ /// Attaches allocator reservation tracking to the current owner.
+ ///
+ /// The allocator that owns the reservation.
+ /// The reserved allocation size, in bytes.
+ ///
+ /// Must complete-before the owning object's reference is observable to any other thread.
+ /// 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 on another thread.
+ ///
+ internal void Attach(MemoryAllocator allocator, long lengthInBytes)
+ {
+ this.allocator = allocator;
+ this.lengthInBytes = lengthInBytes;
+ }
+
+ ///
+ /// Releases the attached allocator reservation once.
+ ///
+ internal void Release()
+ {
+ if (Interlocked.Exchange(ref this.released, 1) == 0 && this.allocator != null)
+ {
+ this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes);
+ this.allocator = null;
+ }
+ }
+}
diff --git a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
index 3ead1c5df7..986ed7f7cf 100644
--- a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs
+++ b/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;
+///
+/// Provides helper methods for working with .
+///
internal static class AllocationOptionsExtensions
{
- public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag;
+ ///
+ /// Returns a value indicating whether the specified flag is set on the allocation options.
+ ///
+ /// The allocation options to inspect.
+ /// The flag to test for.
+ /// if is set; otherwise, .
+ public static bool Has(this AllocationOptions options, AllocationOptions flag)
+ => (options & flag) == flag;
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
index 9f34602fb1..c22e827a8a 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs
@@ -47,7 +47,7 @@ public BasicArrayBuffer(T[] array)
public override Span GetSpan() => this.Array.AsSpan(0, this.Length);
///
- protected override void Dispose(bool disposing)
+ protected override void DisposeCore(bool disposing)
{
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
index a6ed797d6d..84dd065f54 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
/// Provides a base class for implementations by implementing pinning logic for adaption.
///
/// The element type.
-internal abstract class ManagedBufferBase : MemoryManager
+internal abstract class ManagedBufferBase : AllocationTrackedMemoryManager
where T : struct
{
private GCHandle pinHandle;
diff --git a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
index 4a202a96c3..b0fe0b6496 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
@@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
///
internal abstract class RefCountedMemoryLifetimeGuard : IDisposable
{
+ private AllocationTrackingState allocationTracking;
private int refCount = 1;
private int disposed;
private int released;
@@ -38,6 +39,14 @@ protected RefCountedMemoryLifetimeGuard()
public void ReleaseRef() => this.ReleaseRef(false);
+ ///
+ /// Attaches allocator reservation tracking to this lifetime guard.
+ ///
+ /// The allocator that owns the reservation.
+ /// The reserved allocation size, in bytes.
+ 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 @@ private void ReleaseRef(bool finalizing)
}
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();
}
}
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
index 02bdf0f48d..97fab1b680 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
@@ -13,7 +13,7 @@ internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted
where T : struct
{
private readonly int lengthInBytes;
- private LifetimeGuard lifetimeGuard;
+ private readonly LifetimeGuard lifetimeGuard;
public SharedArrayPoolBuffer(int lengthInElements)
{
@@ -24,7 +24,10 @@ public SharedArrayPoolBuffer(int lengthInElements)
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)
{
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
index 854b40e0c7..3a729cdf2b 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
/// access to unmanaged buffers allocated by .
///
/// The element type.
-internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted
+internal sealed unsafe class UnmanagedBuffer : AllocationTrackedMemoryManager, IRefCounted
where T : struct
{
private readonly int lengthInElements;
@@ -31,6 +31,9 @@ public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifeti
public void* Pointer => this.lifetimeGuard.Handle.Pointer;
+ protected internal override void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
+ => this.lifetimeGuard.AttachAllocationTracking(allocator, lengthInBytes);
+
public override Span GetSpan()
{
DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name);
@@ -52,7 +55,7 @@ public override MemoryHandle Pin(int elementIndex = 0)
}
///
- protected override void Dispose(bool disposing)
+ protected override void DisposeCore(bool disposing)
{
DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!");
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
index 8eaf0b6d69..1ca0df1c94 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
+++ b/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;
///
/// Gets the default platform-specific global instance that
@@ -23,9 +24,34 @@ public abstract class MemoryAllocator
///
public static MemoryAllocator Default { get; } = Create();
- internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
+ ///
+ /// Gets the maximum number of bytes that can be allocated by a memory group.
+ ///
+ ///
+ /// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and
+ /// 1 GB for 32-bit processes.
+ ///
+ internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
- internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
+ ///
+ /// Gets the maximum accumulative size, in bytes, of all active allocations made through this allocator instance.
+ ///
+ ///
+ /// Defaults to , 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.
+ /// When the accumulative size of active allocations exceeds this limit, an will be thrown to
+ /// prevent further allocations and signal that the limit has been breached.
+ ///
+ internal long AccumulativeAllocationLimitBytes { get; private protected set; } = long.MaxValue;
+
+ ///
+ /// Gets the maximum size, in bytes, that can be allocated for a single buffer.
+ ///
+ ///
+ /// The single buffer allocation limit is set to 1 GB by default.
+ ///
+ internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte;
///
/// 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;
+ }
+
+ ///
+ /// Applies the supplied to this instance.
+ ///
+ /// The options to apply. Properties left as are ignored.
+ 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;
+ }
}
///
@@ -63,15 +102,60 @@ public static MemoryAllocator Create(MemoryAllocatorOptions options)
/// Size of the buffer to allocate.
/// The allocation options.
/// A buffer of values of type .
- /// When length is zero or negative.
- /// When length is over the capacity of the allocator.
- public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ /// When length is negative or over the capacity of the allocator.
+ public IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ {
+ long lengthInBytes = this.GetValidatedAllocationLengthInBytes(length);
+ bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && lengthInBytes != 0;
+ if (shouldTrack)
+ {
+ this.ReserveAllocation(lengthInBytes);
+ }
+
+ try
+ {
+ AllocationTrackedMemoryManager owner = this.AllocateCore(length, options);
+ if (shouldTrack)
+ {
+ owner.AttachAllocationTracking(this, lengthInBytes);
+ }
+
+ return owner;
+ }
+ catch
+ {
+ if (shouldTrack)
+ {
+ this.ReleaseAccumulatedBytes(lengthInBytes);
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// Allocates a tracked memory owner for .
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the buffer to allocate.
+ /// The allocation options.
+ /// A tracked memory owner of values of type .
+ ///
+ /// 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.
+ ///
+ protected abstract AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
where T : struct;
///
/// Releases all retained resources not being in use.
/// Eg: by resetting array pools and letting GC to free the arrays.
///
+ ///
+ /// This does not dispose active allocations; callers are responsible for disposing all
+ /// instances to release memory.
+ ///
public virtual void ReleaseRetainedResources()
{
}
@@ -102,11 +186,109 @@ internal MemoryGroup AllocateGroup(
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(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
+ long totalLengthInBytesLong = (long)totalLengthInBytes;
+ bool shouldTrack = this.AccumulativeAllocationLimitBytes != long.MaxValue && totalLengthInBytesLong != 0;
+ if (shouldTrack)
+ {
+ this.ReserveAllocation(totalLengthInBytesLong);
+ }
+
+ try
+ {
+ MemoryGroup group = this.AllocateGroupCore(totalLength, totalLengthInBytesLong, bufferAlignment, options);
+ if (shouldTrack)
+ {
+ group.AttachAllocationTracking(this, totalLengthInBytesLong);
+ }
+
+ return group;
+ }
+ catch
+ {
+ if (shouldTrack)
+ {
+ this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
+ }
+
+ throw;
+ }
}
internal virtual MemoryGroup AllocateGroupCore(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
where T : struct
=> MemoryGroup.Allocate(this, totalLengthInElements, bufferAlignment, options);
+
+ ///
+ /// Allocates a single segment for construction.
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the segment to allocate.
+ /// The allocation options.
+ /// A segment owner for the requested buffer length.
+ ///
+ /// The default implementation validates the segment size then calls
+ /// directly so group construction can reserve and release the total allocation once.
+ ///
+ internal virtual IMemoryOwner AllocateGroupBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ {
+ _ = this.GetValidatedAllocationLengthInBytes(length);
+ return this.AllocateCore(length, options);
+ }
+
+ ///
+ /// Returns the validated allocation length in bytes.
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the buffer to allocate.
+ /// The allocation length in bytes.
+ private long GetValidatedAllocationLengthInBytes(int length)
+ where T : struct
+ {
+ if (length < 0)
+ {
+ InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
+ }
+
+ ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
+ if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
+ {
+ InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
+ }
+
+ return (long)lengthInBytes;
+ }
+
+ ///
+ /// Reserves accumulative allocation bytes before creating the underlying buffer.
+ ///
+ /// The number of bytes to reserve.
+ 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);
+ }
+ }
+
+ ///
+ /// Releases accumulative allocation bytes previously tracked by this allocator.
+ ///
+ /// The number of bytes to release.
+ internal void ReleaseAccumulatedBytes(long lengthInBytes)
+ {
+ if (lengthInBytes <= 0)
+ {
+ return;
+ }
+
+ _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
+ }
}
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
index d9ba62c1ef..41a5ea5ee3 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
@@ -10,6 +10,7 @@ public struct MemoryAllocatorOptions
{
private int? maximumPoolSizeMegabytes;
private int? allocationLimitMegabytes;
+ private int? accumulativeAllocationLimitMegabytes;
///
/// Gets or sets a value defining the maximum size of the 's internal memory pool
@@ -17,7 +18,7 @@ public struct MemoryAllocatorOptions
///
public int? MaximumPoolSizeMegabytes
{
- get => this.maximumPoolSizeMegabytes;
+ readonly get => this.maximumPoolSizeMegabytes;
set
{
if (value.HasValue)
@@ -35,15 +36,48 @@ public int? MaximumPoolSizeMegabytes
///
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;
}
}
+
+ ///
+ /// Gets or sets a value defining the maximum accumulative size, in Megabytes, of all active allocations made
+ /// through the created instance.
+ /// (the default) imposes no limit on the accumulative total.
+ ///
+ 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;
+ }
+ }
}
diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
index 675afe8b9f..5d183fa442 100644
--- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
+++ b/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;
///
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
+ ///
+ /// Initializes a new instance of the class with default limits.
+ ///
+ public SimpleGcMemoryAllocator()
+ : this(default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with custom limits.
+ ///
+ /// The to apply.
+ public SimpleGcMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options);
+
///
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
///
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
- {
- if (length < 0)
- {
- InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
- }
-
- ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
- if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
- {
- InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
- }
-
- return new BasicArrayBuffer(new T[length]);
- }
+ protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
+ => new BasicArrayBuffer(new T[length]);
}
diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
index d5cd329f1b..cfffc679a2 100644
--- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
+++ b/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 UniformUnmanagedMemoryPoolMemoryAllocator(
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);
+
///
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes;
///
- public override IMemoryOwner Allocate(
+ protected override AllocationTrackedMemoryManager AllocateCore(
int length,
AllocationOptions options = AllocationOptions.None)
{
- if (length < 0)
- {
- InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
- }
-
- ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
- if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
- {
- InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
- }
-
- if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
+ int lengthInBytes = length * Unsafe.SizeOf();
+ if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
SharedArrayPoolBuffer buffer = new(length);
if (options.Has(AllocationOptions.Clean))
@@ -101,17 +99,16 @@ public override IMemoryOwner Allocate(
return buffer;
}
- if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
+ if (lengthInBytes <= this.poolBufferSizeInBytes)
{
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
- UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean));
- return buffer;
+ return this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean));
}
}
- return this.nonPoolAllocator.Allocate(length, options);
+ return UnmanagedMemoryAllocator.AllocateBuffer(length, options);
}
///
diff --git a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
index daf1a79925..eb52da7c03 100644
--- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
@@ -18,7 +18,14 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes;
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ => AllocateBuffer(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 AllocateBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
{
UnmanagedBuffer buffer = UnmanagedBuffer.Allocate(length);
if (options.Has(AllocationOptions.Clean))
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
index 7e9719ea75..03f26aab04 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
@@ -15,12 +15,12 @@ public interface IMemoryGroup : IReadOnlyList>
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller.
///
- int BufferLength { get; }
+ public int BufferLength { get; }
///
/// Gets the aggregate number of elements in the group.
///
- long TotalLength { get; }
+ public long TotalLength { get; }
///
/// Gets a value indicating whether the group has been invalidated.
@@ -29,7 +29,7 @@ public interface IMemoryGroup : IReadOnlyList>
/// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces
/// the image buffers internally.
///
- bool IsValid { get; }
+ public bool IsValid { get; }
///
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
@@ -39,5 +39,5 @@ public interface IMemoryGroup : IReadOnlyList>
/// implementation, which is still available when casting to one of the underlying interfaces.
///
/// A new instance mapping the current values in use.
- new MemoryGroupEnumerator GetEnumerator();
+ public new MemoryGroupEnumerator GetEnumerator();
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
index 950e2a019e..75e93ce7f8 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
@@ -31,23 +31,23 @@ public override int Count
///
[MethodImpl(InliningOptions.ShortMethod)]
- public override MemoryGroupEnumerator GetEnumerator()
- {
- return new MemoryGroupEnumerator(this);
- }
+ public override MemoryGroupEnumerator GetEnumerator() => new(this);
///
IEnumerator> IEnumerable>.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> (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>)this.source).GetEnumerator();
- }
+ => ((IList>)this.source).GetEnumerator();
- public override void Dispose() => this.View.Invalidate();
+ public override void Dispose()
+ {
+ this.View.Invalidate();
+ this.ReleaseAllocationTracking();
+ }
}
}
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
index af896ee0e1..c4b22f1560 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
@@ -60,6 +60,58 @@ public override Memory this[int index]
}
}
+ 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[]? memoryOwners = this.memoryOwners;
+ if (memoryOwners?.Length == 1 && memoryOwners[0] is AllocationTrackedMemoryManager 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 memoryOwner in memoryOwners)
+ {
+ if (memoryOwner is not AllocationTrackedMemoryManager)
+ {
+ // 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)memoryOwners[i];
+ long ownerLengthInBytes = (long)trackedOwner.Memory.Length * Unsafe.SizeOf();
+ trackedOwner.AttachAllocationTracking(allocator, ownerLengthInBytes);
+ remainingLengthInBytes -= ownerLengthInBytes;
+ }
+
+ trackedOwner = (AllocationTrackedMemoryManager)memoryOwners[lastOwnerIndex];
+ trackedOwner.AttachAllocationTracking(allocator, remainingLengthInBytes);
+ return;
+ }
+
+ base.AttachAllocationTracking(allocator, lengthInBytes);
+ }
+
private static IMemoryOwner[] CreateBuffers(
UnmanagedMemoryHandle[] pooledBuffers,
int bufferLength,
@@ -73,8 +125,8 @@ private static IMemoryOwner[] CreateBuffers(
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 @@ public override void Dispose()
}
}
+ this.ReleaseAllocationTracking();
this.memoryOwners = null;
this.IsValid = false;
this.groupLifetimeGuard = null;
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
index 6dd99fcb02..e0b9bca5e2 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
@@ -21,6 +21,7 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
{
private static readonly int ElementSize = Unsafe.SizeOf();
+ private AllocationTrackingState allocationTracking;
private MemoryGroupSpanCache memoryGroupSpanCache;
private MemoryGroup(int bufferLength, long totalLength)
@@ -52,16 +53,36 @@ private MemoryGroup(int bufferLength, long totalLength)
///
public abstract MemoryGroupEnumerator GetEnumerator();
+ ///
+ /// Attaches allocation tracking by specifying the allocator and the length, in bytes, to be tracked.
+ ///
+ /// The memory allocator to use for tracking allocations.
+ /// The length, in bytes, of the memory region to track. Must be greater than or equal to zero.
+ ///
+ /// Intended for one-time initialization after the group has been created; callers should avoid changing
+ /// tracking state concurrently with disposal.
+ ///
+ internal virtual void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) =>
+ this.allocationTracking.Attach(allocator, lengthInBytes);
+
+ ///
+ /// Releases any resources or tracking information associated with allocation tracking for this instance.
+ ///
+ ///
+ /// 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.
+ ///
+ internal void ReleaseAllocationTracking() => this.allocationTracking.Release();
+
///
IEnumerator> IEnumerable>.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>.GetEnumerator()");
- }
+ => throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable>.GetEnumerator()");
///
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable>)this).GetEnumerator();
@@ -81,8 +102,8 @@ public static MemoryGroup Allocate(
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 @@ public static MemoryGroup Allocate(
if (totalLengthInElements == 0)
{
- IMemoryOwner[] buffers0 = [allocator.Allocate(0, options)];
- return new Owned(buffers0, 0, 0, true);
+ IMemoryOwner[] emptyBuffer = [allocator.AllocateGroupBuffer(0, options)];
+ return new Owned(emptyBuffer, 0, 0, true);
}
int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements;
@@ -123,12 +144,12 @@ public static MemoryGroup Allocate(
IMemoryOwner[] buffers = new IMemoryOwner[bufferCount];
for (int i = 0; i < buffers.Length - 1; i++)
{
- buffers[i] = allocator.Allocate(bufferLength, options);
+ buffers[i] = allocator.AllocateGroupBuffer(bufferLength, options);
}
if (bufferCount > 0)
{
- buffers[^1] = allocator.Allocate(sizeOfLastBuffer, options);
+ buffers[^1] = allocator.AllocateGroupBuffer(sizeOfLastBuffer, options);
}
return new Owned(buffers, bufferLength, totalLengthInElements, true);
diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
index 81210f13db..724af35e1d 100644
--- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
+++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
@@ -39,4 +39,9 @@ internal static void ThrowInvalidAlignmentException(long alignment) =>
[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}.");
}
diff --git a/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
index 27e42f84e2..a5dfb0d23f 100644
--- a/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
+++ b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
@@ -313,7 +313,15 @@ public MockUnmanagedMemoryAllocator(params UnmanagedBuffer[] buffers)
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) =>
- this.buffers.Pop() as IMemoryOwner;
+ protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
+ {
+ object buffer = this.buffers.Pop();
+ if (buffer is AllocationTrackedMemoryManager trackedBuffer)
+ {
+ return trackedBuffer;
+ }
+
+ throw new InvalidMemoryOperationException("The requested buffer type does not match the mock allocator buffer type.");
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
index 0e791c5d97..c33e6d1070 100644
--- a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
+++ b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
@@ -48,6 +48,50 @@ public unsafe void Allocate_MemoryIsPinnableMultipleTimes()
}
}
+ [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 b0 = allocator.Allocate(oneMb);
+
+ // Additional allocation should exceed the limit while the owner is live.
+ Assert.Throws(() => allocator.Allocate(1));
+
+ // Disposing the owner releases the reservation.
+ b0.Dispose();
+
+ // Allocation should succeed after the reservation is released.
+ allocator.Allocate(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 g0 = allocator.AllocateGroup(oneMb, 1024);
+
+ // Additional allocation should exceed the limit while the group is live.
+ Assert.Throws(() => allocator.AllocateGroup(1, 1024));
+
+ // Disposing the group releases the reservation.
+ g0.Dispose();
+
+ // Allocation should succeed after the reservation is released.
+ allocator.AllocateGroup(oneMb, 1024).Dispose();
+ }
+
[StructLayout(LayoutKind.Explicit, Size = 512)]
private struct BigStruct
{
diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
index 1d185a0de9..b4593d4d09 100644
--- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
+++ b/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 BufferTests1()
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 @@ static void RunTest()
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 g2 = allocator.AllocateGroup(B(8), 1024);
using MemoryGroup g3 = allocator.AllocateGroup(B(8), 1024);
@@ -323,7 +323,7 @@ static void RunTest(string lengthStr)
}
[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 @@ static void RunTest(string lengthStr)
}
}
+ [Theory]
+ [InlineData(1)] // SharedArrayPoolBuffer
+ [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(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 g = allocator.Allocate(length);
@@ -409,6 +435,30 @@ private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllo
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(length, 1024).Dispose();
+ }
+ }
+
[Fact]
public void Allocate_OverLimit_ThrowsInvalidMemoryOperationException()
{
@@ -433,6 +483,107 @@ public void AllocateGroup_OverLimit_ThrowsInvalidMemoryOperationException()
Assert.Throws(() => allocator.AllocateGroup(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 b0 = allocator.Allocate(oneMb);
+
+ // Additional allocation should exceed the limit while the owner is live.
+ Assert.Throws(() => allocator.Allocate(1));
+
+ // Disposing the owner releases the reservation.
+ b0.Dispose();
+
+ // Allocation should succeed after the reservation is released.
+ allocator.Allocate(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 g0 = allocator.AllocateGroup(oneMb, 1024);
+
+ // Additional allocation should exceed the limit while the group is live.
+ Assert.Throws(() => allocator.AllocateGroup(1, 1024));
+
+ // Disposing the group releases the reservation.
+ g0.Dispose();
+
+ // Allocation should succeed after the reservation is released.
+ allocator.AllocateGroup(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.TryAllocate(pool, ...) and fall
+ // through to MemoryGroup.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 g = allocator.AllocateGroup(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(() => allocator.Allocate(512 * 1024));
+
+ g.Dispose();
+
+ // After disposal the reservation is fully released; a second equivalent group succeeds.
+ allocator.AllocateGroup(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(768 * 1024, 1024).Dispose();
+ }
+ }
+
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
public void MemoryAllocator_Create_SetHighLimit()
{
diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
index 678a089a85..cca2230e6f 100644
--- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
+++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
@@ -98,7 +98,15 @@ public void Allocate_FromPool_BufferSizesAreCorrect(
[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)
{
diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs
index a0ff4a466e..f9a4c0a6a8 100644
--- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs
@@ -37,7 +37,7 @@ public void EnableNonThreadSafeLogging()
this.returnLog = new List();
}
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
{
T[] array = this.AllocateArray(length, options);
return new BasicArrayBuffer(array, length, this);
@@ -110,7 +110,7 @@ public ReturnRequest(int hashCodeOfBuffer)
///
/// Wraps an array as an instance.
///
- private class BasicArrayBuffer : MemoryManager
+ private class BasicArrayBuffer : AllocationTrackedMemoryManager
where T : struct
{
private readonly TestMemoryAllocator allocator;
@@ -159,7 +159,7 @@ public override void Unpin()
}
///
- protected override void Dispose(bool disposing)
+ protected override void DisposeCore(bool disposing)
{
if (disposing)
{