diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
index 7a694aea26..1ca0df1c94 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
@@ -34,13 +34,13 @@ public abstract class MemoryAllocator
internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
///
- /// Gets the maximum cumulative size, in bytes, of all active allocations made through this allocator instance.
+ /// Gets the maximum accumulative size, in bytes, of all active allocations made through this allocator instance.
///
///
- /// Defaults to , effectively imposing no limit on the cumulative total.
+ /// 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 cumulative size of active allocations exceeds this limit, an will be thrown to
+ /// 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;
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
index e02dff6f1b..41a5ea5ee3 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
@@ -42,6 +42,13 @@ public struct MemoryAllocatorOptions
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;
@@ -49,9 +56,9 @@ public struct MemoryAllocatorOptions
}
///
- /// Gets or sets a value defining the maximum cumulative size, in Megabytes, of all active allocations made
+ /// 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 cumulative total.
+ /// (the default) imposes no limit on the accumulative total.
///
public int? AccumulativeAllocationLimitMegabytes
{
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
index f1ec63b16e..c4b22f1560 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
@@ -78,6 +78,37 @@ internal abstract partial class MemoryGroup
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);
}
diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
index 2722747a1b..724af35e1d 100644
--- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
+++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
@@ -43,5 +43,5 @@ public class InvalidMemoryOperationException : InvalidOperationException
[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 cumulative allocation size to {totalLength}, exceeding the limit {limit}.");
+ $"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/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
index fe51748522..b4593d4d09 100644
--- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
+++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
@@ -558,6 +558,32 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
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()
{