diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
new file mode 100644
index 0000000000..cb71bdfd40
--- /dev/null
+++ b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+
+namespace SixLabors.ImageSharp.Memory;
+
+///
+/// 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)
+ {
+ this.DisposeCore(disposing);
+ 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.
+ ///
+ internal 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..a0c22ccebf
--- /dev/null
+++ b/src/ImageSharp/Memory/AllocationTrackingState.cs
@@ -0,0 +1,41 @@
+// 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.
+ 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/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
index 02bdf0f48d..723be26f2b 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,7 @@ public SharedArrayPoolBuffer(int lengthInElements)
public byte[]? Array { get; private set; }
- protected override void Dispose(bool disposing)
+ 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..d9a9c5db25 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;
@@ -52,7 +52,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..48ae8a479d 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
@@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Memory;
public abstract class MemoryAllocator
{
private const int OneGigabyte = 1 << 30;
+ private long accumulativeAllocatedBytes;
+ private int trackingSuppressionCount;
///
/// Gets the default platform-specific global instance that
@@ -23,9 +25,43 @@ 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 allowed total allocation size, in bytes, for the current process.
+ ///
+ ///
+ /// Defaults to , effectively imposing no limit on total allocations.
+ /// This property can be set to enforce a cap on total memory usage across all allocations made through this allocator instance, providing
+ /// a safeguard against excessive memory consumption.
+ /// When the cumulative 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 a value indicating whether accumulative allocation tracking is currently suppressed for this instance.
+ ///
+ ///
+ /// This is used internally when an outer allocator or memory group reservation already owns the tracked bytes
+ /// and nested allocations must not reserve or release them a second time.
+ ///
+ private bool IsTrackingSuppressed => Volatile.Read(ref this.trackingSuppressionCount) > 0;
///
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@@ -53,6 +89,11 @@ public static MemoryAllocator Create(MemoryAllocatorOptions options)
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
}
+ if (options.AccumulativeAllocationLimitMegabytes.HasValue)
+ {
+ allocator.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
+ }
+
return allocator;
}
@@ -63,15 +104,72 @@ public 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 negative.
/// When length is over the capacity of the allocator.
- public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ public IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ 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);
+ }
+
+ long lengthInBytesLong = (long)lengthInBytes;
+ bool shouldTrack = !this.IsTrackingSuppressed && lengthInBytesLong != 0;
+ if (shouldTrack)
+ {
+ this.ReserveAllocation(lengthInBytesLong);
+ }
+
+ try
+ {
+ AllocationTrackedMemoryManager owner = this.AllocateCore(length, options);
+ if (shouldTrack)
+ {
+ owner.AttachAllocationTracking(this, lengthInBytesLong);
+ }
+
+ return owner;
+ }
+ catch
+ {
+ if (shouldTrack)
+ {
+ this.ReleaseAccumulatedBytes(lengthInBytesLong);
+ }
+
+ 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 +200,119 @@ 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.IsTrackingSuppressed && totalLengthInBytesLong != 0;
+ if (shouldTrack)
+ {
+ this.ReserveAllocation(totalLengthInBytesLong);
+ }
+
+ using (this.SuppressTracking())
+ {
+ 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 uses . Built-in allocators
+ /// can override this to supply raw segment owners when group construction must bypass nested tracking.
+ ///
+ internal virtual IMemoryOwner AllocateGroupBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct
+ => this.Allocate(length, options);
+
+ ///
+ /// Reserves accumulative allocation bytes before creating the underlying buffer.
+ ///
+ /// The number of bytes to reserve.
+ private void ReserveAllocation(long lengthInBytes)
+ {
+ if (this.IsTrackingSuppressed || lengthInBytes <= 0)
+ {
+ return;
+ }
+
+ long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes);
+ if (total > this.AccumulativeAllocationLimitBytes)
+ {
+ _ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
+ InvalidMemoryOperationException.ThrowAllocationOverLimitException((ulong)lengthInBytes, this.AccumulativeAllocationLimitBytes);
+ }
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
+ ///
+ /// A scope that restores tracking when disposed.
+ ///
+ /// Returning the concrete scope type keeps nested allocator calls allocation-free on the hot path
+ /// while preserving the same using-pattern at call sites.
+ ///
+ private TrackingSuppressionScope SuppressTracking() => new(this);
+
+ ///
+ /// Temporarily suppresses accumulative allocation tracking within a scope.
+ ///
+ private struct TrackingSuppressionScope : IDisposable
+ {
+ private MemoryAllocator? allocator;
+
+ public TrackingSuppressionScope(MemoryAllocator allocator)
+ {
+ this.allocator = allocator;
+ _ = Interlocked.Increment(ref allocator.trackingSuppressionCount);
+ }
+
+ public void Dispose()
+ {
+ if (this.allocator != null)
+ {
+ _ = Interlocked.Decrement(ref this.allocator.trackingSuppressionCount);
+ this.allocator = null;
+ }
+ }
+ }
}
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
index d9ba62c1ef..3339203dac 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,7 +36,7 @@ public int? MaximumPoolSizeMegabytes
///
public int? AllocationLimitMegabytes
{
- get => this.allocationLimitMegabytes;
+ readonly get => this.allocationLimitMegabytes;
set
{
if (value.HasValue)
@@ -46,4 +47,29 @@ public int? AllocationLimitMegabytes
this.allocationLimitMegabytes = value;
}
}
+
+ ///
+ /// Gets or sets a value defining the maximum total size that can be allocated by the allocator in Megabytes.
+ /// means platform default: 2GB on 32-bit processes, 8GB on 64-bit processes.
+ ///
+ 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..162bc85cad 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,36 @@ namespace SixLabors.ImageSharp.Memory;
///
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
- ///
- protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
+ ///
+ /// Initializes a new instance of the class with default limits.
+ ///
+ public SimpleGcMemoryAllocator()
+ : this(default)
+ {
+ }
- ///
- public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ ///
+ /// Initializes a new instance of the class with custom limits.
+ ///
+ /// The to apply.
+ public SimpleGcMemoryAllocator(MemoryAllocatorOptions options)
{
- if (length < 0)
+ if (options.AllocationLimitMegabytes.HasValue)
{
- InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
+ this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
+ this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
}
- ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
- if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
+ if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
- InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
+ this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
-
- return new BasicArrayBuffer(new T[length]);
}
+
+ ///
+ protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
+
+ ///
+ 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..ad485f9271 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;
@@ -75,22 +74,12 @@ internal UniformUnmanagedMemoryPoolMemoryAllocator(
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 +90,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..69a4af70ed 100644
--- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
@@ -18,7 +18,18 @@ 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);
+
+ internal override IMemoryOwner AllocateGroupBuffer(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..be92272bbe 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
@@ -73,8 +73,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 +155,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..870c852d50 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 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();
@@ -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/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
index 27e42f84e2..cbd6a9e147 100644
--- a/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
+++ b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs
@@ -313,7 +313,7 @@ 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) =>
+ this.buffers.Pop() as AllocationTrackedMemoryManager;
}
}
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..06da3505e1 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);
@@ -433,6 +433,50 @@ 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();
+ }
+
[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)
{