From 523aacdfb131126bae40bc6b3e469ed43e0086b5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 20 Apr 2026 00:10:50 +1000 Subject: [PATCH 1/3] Add AllocationTrackedMemoryManager and refactor allocators --- .../AllocationTrackedMemoryManager{T}.cs | 73 ++++++ .../Allocators/AllocationOptionsExtensions.cs | 14 +- .../Allocators/Internals/BasicArrayBuffer.cs | 2 +- .../Allocators/Internals/ManagedBufferBase.cs | 2 +- .../Internals/SharedArrayPoolBuffer{T}.cs | 2 +- .../Internals/UnmanagedBuffer{T}.cs | 4 +- .../Memory/Allocators/MemoryAllocator.cs | 218 +++++++++++++++++- .../Allocators/MemoryAllocatorOptions.cs | 30 ++- .../Allocators/SimpleGcMemoryAllocator.cs | 35 ++- ...iformUnmanagedMemoryPoolMemoryAllocator.cs | 23 +- .../Allocators/UnmanagedMemoryAllocator.cs | 14 +- .../DiscontiguousBuffers/IMemoryGroup{T}.cs | 8 +- .../MemoryGroup{T}.Consumed.cs | 16 +- .../MemoryGroup{T}.Owned.cs | 5 +- .../DiscontiguousBuffers/MemoryGroup{T}.cs | 47 +++- .../Image/ProcessPixelRowsTestBase.cs | 4 +- .../SimpleGcMemoryAllocatorTests.cs | 44 ++++ ...niformUnmanagedPoolMemoryAllocatorTests.cs | 56 ++++- .../MemoryGroupTests.Allocate.cs | 10 +- .../TestUtilities/TestMemoryAllocator.cs | 6 +- 20 files changed, 536 insertions(+), 77 deletions(-) create mode 100644 src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs new file mode 100644 index 0000000000..094c4dea4d --- /dev/null +++ b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs @@ -0,0 +1,73 @@ +// 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 MemoryAllocator? trackingAllocator; + private long trackingLengthInBytes; + private int trackingReleased; + + /// + /// 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.trackingAllocator = allocator; + this.trackingLengthInBytes = 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() + { + if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null) + { + this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes); + this.trackingAllocator = 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..ef79b57e62 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs @@ -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..61c19844dc 100644 --- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs @@ -12,23 +12,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..e2b7e86571 100644 --- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -75,22 +75,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 +91,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..315aa1db0d 100644 --- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory; @@ -18,7 +19,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..2f586a74c8 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -22,6 +22,9 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable private static readonly int ElementSize = Unsafe.SizeOf(); private MemoryGroupSpanCache memoryGroupSpanCache; + private MemoryAllocator? trackingAllocator; + private long trackingLengthInBytes; + private int trackingReleased; private MemoryGroup(int bufferLength, long totalLength) { @@ -52,16 +55,46 @@ 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.trackingAllocator = allocator; + this.trackingLengthInBytes = 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() + { + if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null) + { + this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes); + this.trackingAllocator = null; + } + } + /// 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 +130,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 +156,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) { From 631b64ea7d2e81978d75ee0a8558f350f1f8c7a9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 20 Apr 2026 00:44:23 +1000 Subject: [PATCH 2/3] Add AllocationTrackingState and refactor tracking --- .../AllocationTrackedMemoryManager{T}.cs | 18 ++------ .../Memory/AllocationTrackingState.cs | 41 +++++++++++++++++++ .../DiscontiguousBuffers/MemoryGroup{T}.cs | 20 ++------- 3 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/ImageSharp/Memory/AllocationTrackingState.cs diff --git a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs index 094c4dea4d..cb71bdfd40 100644 --- a/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs +++ b/src/ImageSharp/Memory/AllocationTrackedMemoryManager{T}.cs @@ -17,9 +17,7 @@ namespace SixLabors.ImageSharp.Memory; public abstract class AllocationTrackedMemoryManager : MemoryManager where T : struct { - private MemoryAllocator? trackingAllocator; - private long trackingLengthInBytes; - private int trackingReleased; + private AllocationTrackingState allocationTracking; /// /// Releases resources held by the concrete tracked owner. @@ -51,10 +49,7 @@ protected sealed override void Dispose(bool disposing) /// Derived allocators should not call it themselves; they only construct the concrete owner. /// internal void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes) - { - this.trackingAllocator = allocator; - this.trackingLengthInBytes = lengthInBytes; - } + => this.allocationTracking.Attach(allocator, lengthInBytes); /// /// Releases any tracked allocation bytes associated with this instance. @@ -62,12 +57,5 @@ internal void AttachAllocationTracking(MemoryAllocator allocator, long lengthInB /// /// Calling this more than once is safe; only the first call after tracking has been attached releases bytes. /// - private void ReleaseAllocationTracking() - { - if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null) - { - this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes); - this.trackingAllocator = null; - } - } + 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/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs index 2f586a74c8..870c852d50 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -21,10 +21,8 @@ internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable { private static readonly int ElementSize = Unsafe.SizeOf(); + private AllocationTrackingState allocationTracking; private MemoryGroupSpanCache memoryGroupSpanCache; - private MemoryAllocator? trackingAllocator; - private long trackingLengthInBytes; - private int trackingReleased; private MemoryGroup(int bufferLength, long totalLength) { @@ -64,11 +62,8 @@ private MemoryGroup(int bufferLength, long totalLength) /// 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.trackingAllocator = allocator; - this.trackingLengthInBytes = lengthInBytes; - } + 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. @@ -77,14 +72,7 @@ internal void AttachAllocationTracking(MemoryAllocator allocator, long lengthInB /// 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() - { - if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null) - { - this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes); - this.trackingAllocator = null; - } - } + internal void ReleaseAllocationTracking() => this.allocationTracking.Release(); /// IEnumerator> IEnumerable>.GetEnumerator() From 15fd9413f8f86eb972cd517f890a423ebd83d3d5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 22 Apr 2026 22:45:45 +1000 Subject: [PATCH 3/3] Cleanup --- .../Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs | 2 +- src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs | 2 -- .../Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs | 1 - src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs index ef79b57e62..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) { diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs index 61c19844dc..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; diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs index e2b7e86571..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; diff --git a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs index 315aa1db0d..69a4af70ed 100644 --- a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory;