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() {