@@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Memory;
1212public abstract class MemoryAllocator
1313{
1414 private const int OneGigabyte = 1 << 30 ;
15+ private long accumulativeAllocatedBytes ;
16+ private int trackingSuppressionCount ;
1517
1618 /// <summary>
1719 /// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
@@ -23,9 +25,44 @@ public abstract class MemoryAllocator
2325 /// </summary>
2426 public static MemoryAllocator Default { get ; } = Create ( ) ;
2527
26- internal long MemoryGroupAllocationLimitBytes { get ; private set ; } = Environment . Is64BitProcess ? 4L * OneGigabyte : OneGigabyte ;
28+ /// <summary>
29+ /// Gets the maximum number of bytes that can be allocated by a memory group.
30+ /// </summary>
31+ /// <remarks>
32+ /// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and
33+ /// 1 GB for 32-bit processes.
34+ /// </remarks>
35+ internal long MemoryGroupAllocationLimitBytes { get ; private protected set ; } = Environment . Is64BitProcess ? 4L * OneGigabyte : OneGigabyte ;
36+
37+ /// <summary>
38+ /// Gets the maximum allowed total allocation size, in bytes, for the current process.
39+ /// </summary>
40+ /// <remarks>
41+ /// Defaults to <see cref="long.MaxValue"/>, effectively imposing no limit on total allocations.
42+ /// This property can be set to enforce a cap on total memory usage across all allocations made through this allocator instance, providing
43+ /// a safeguard against excessive memory consumption.<br/>
44+ /// When the cumulative size of active allocations exceeds this limit, an <see cref="InvalidMemoryOperationException"/> will be thrown to
45+ /// prevent further allocations and signal that the limit has been breached.
46+ /// </remarks>
47+ internal long AccumulativeAllocationLimitBytes { get ; private protected set ; } = long . MaxValue ;
2748
28- internal int SingleBufferAllocationLimitBytes { get ; private set ; } = OneGigabyte ;
49+ /// <summary>
50+ /// Gets the maximum size, in bytes, that can be allocated for a single buffer.
51+ /// </summary>
52+ /// <remarks>
53+ /// The single buffer allocation limit is set to 1 GB by default.
54+ /// </remarks>
55+ internal int SingleBufferAllocationLimitBytes { get ; private protected set ; } = OneGigabyte ;
56+
57+ /// <summary>
58+ /// Gets a value indicating whether change tracking is currently suppressed for this instance.
59+ /// </summary>
60+ /// <remarks>
61+ /// When change tracking is suppressed, modifications to the object will not be recorded or
62+ /// trigger change notifications. This property is used internally to temporarily disable tracking during
63+ /// batch updates or initialization.
64+ /// </remarks>
65+ private bool IsTrackingSuppressed => Volatile . Read ( ref this . trackingSuppressionCount ) > 0 ;
2966
3067 /// <summary>
3168 /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@@ -53,6 +90,11 @@ public static MemoryAllocator Create(MemoryAllocatorOptions options)
5390 allocator . SingleBufferAllocationLimitBytes = ( int ) Math . Min ( allocator . SingleBufferAllocationLimitBytes , allocator . MemoryGroupAllocationLimitBytes ) ;
5491 }
5592
93+ if ( options . AccumulativeAllocationLimitMegabytes . HasValue )
94+ {
95+ allocator . AccumulativeAllocationLimitBytes = options . AccumulativeAllocationLimitMegabytes . Value * 1024L * 1024L ;
96+ }
97+
5698 return allocator ;
5799 }
58100
@@ -72,6 +114,10 @@ public abstract IMemoryOwner<T> Allocate<T>(int length, AllocationOptions option
72114 /// Releases all retained resources not being in use.
73115 /// Eg: by resetting array pools and letting GC to free the arrays.
74116 /// </summary>
117+ /// <remarks>
118+ /// This does not dispose active allocations; callers are responsible for disposing all
119+ /// <see cref="IMemoryOwner{T}"/> instances to release memory.
120+ /// </remarks>
75121 public virtual void ReleaseRetainedResources ( )
76122 {
77123 }
@@ -102,11 +148,137 @@ internal MemoryGroup<T> AllocateGroup<T>(
102148 InvalidMemoryOperationException . ThrowAllocationOverLimitException ( totalLengthInBytes , this . MemoryGroupAllocationLimitBytes ) ;
103149 }
104150
105- // Cast to long is safe because we already checked that the total length is within the limit.
106- return this . AllocateGroupCore < T > ( totalLength , ( long ) totalLengthInBytes , bufferAlignment , options ) ;
151+ long totalLengthInBytesLong = ( long ) totalLengthInBytes ;
152+ this . ReserveAllocation ( totalLengthInBytesLong ) ;
153+
154+ using ( this . SuppressTracking ( ) )
155+ {
156+ try
157+ {
158+ MemoryGroup < T > group = this . AllocateGroupCore < T > ( totalLength , totalLengthInBytesLong , bufferAlignment , options ) ;
159+ group . SetAllocationTracking ( this , totalLengthInBytesLong ) ;
160+ return group ;
161+ }
162+ catch
163+ {
164+ this . ReleaseAccumulatedBytes ( totalLengthInBytesLong ) ;
165+ throw ;
166+ }
167+ }
107168 }
108169
109170 internal virtual MemoryGroup < T > AllocateGroupCore < T > ( long totalLengthInElements , long totalLengthInBytes , int bufferAlignment , AllocationOptions options )
110171 where T : struct
111172 => MemoryGroup < T > . Allocate ( this , totalLengthInElements , bufferAlignment , options ) ;
173+
174+ /// <summary>
175+ /// Tracks the allocation of an <see cref="IMemoryOwner{T}" /> instance after reserving bytes.
176+ /// </summary>
177+ /// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
178+ /// <param name="owner">The allocation to track.</param>
179+ /// <param name="lengthInBytes">The allocation size in bytes.</param>
180+ /// <returns>The tracked allocation.</returns>
181+ protected IMemoryOwner < T > TrackAllocation < T > ( IMemoryOwner < T > owner , ulong lengthInBytes )
182+ where T : struct
183+ {
184+ if ( this . IsTrackingSuppressed || lengthInBytes == 0 )
185+ {
186+ return owner ;
187+ }
188+
189+ return new TrackingMemoryOwner < T > ( owner , this , ( long ) lengthInBytes ) ;
190+ }
191+
192+ /// <summary>
193+ /// Reserves accumulative allocation bytes before creating the underlying buffer.
194+ /// </summary>
195+ /// <param name="lengthInBytes">The number of bytes to reserve.</param>
196+ protected void ReserveAllocation ( long lengthInBytes )
197+ {
198+ if ( this . IsTrackingSuppressed || lengthInBytes <= 0 )
199+ {
200+ return ;
201+ }
202+
203+ long total = Interlocked . Add ( ref this . accumulativeAllocatedBytes , lengthInBytes ) ;
204+ if ( total > this . AccumulativeAllocationLimitBytes )
205+ {
206+ _ = Interlocked . Add ( ref this . accumulativeAllocatedBytes , - lengthInBytes ) ;
207+ InvalidMemoryOperationException . ThrowAllocationOverLimitException ( ( ulong ) lengthInBytes , this . AccumulativeAllocationLimitBytes ) ;
208+ }
209+ }
210+
211+ /// <summary>
212+ /// Releases accumulative allocation bytes previously tracked by this allocator.
213+ /// </summary>
214+ /// <param name="lengthInBytes">The number of bytes to release.</param>
215+ internal void ReleaseAccumulatedBytes ( long lengthInBytes )
216+ {
217+ if ( lengthInBytes <= 0 )
218+ {
219+ return ;
220+ }
221+
222+ _ = Interlocked . Add ( ref this . accumulativeAllocatedBytes , - lengthInBytes ) ;
223+ }
224+
225+ /// <summary>
226+ /// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
227+ /// </summary>
228+ /// <returns>An <see cref="IDisposable"/> that restores tracking when disposed.</returns>
229+ internal IDisposable SuppressTracking ( ) => new TrackingSuppressionScope ( this ) ;
230+
231+ /// <summary>
232+ /// Temporarily suppresses accumulative allocation tracking within a scope.
233+ /// </summary>
234+ private sealed class TrackingSuppressionScope : IDisposable
235+ {
236+ private MemoryAllocator ? allocator ;
237+
238+ public TrackingSuppressionScope ( MemoryAllocator allocator )
239+ {
240+ this . allocator = allocator ;
241+ _ = Interlocked . Increment ( ref allocator . trackingSuppressionCount ) ;
242+ }
243+
244+ public void Dispose ( )
245+ {
246+ if ( this . allocator != null )
247+ {
248+ _ = Interlocked . Decrement ( ref this . allocator . trackingSuppressionCount ) ;
249+ this . allocator = null ;
250+ }
251+ }
252+ }
253+
254+ /// <summary>
255+ /// Wraps an <see cref="IMemoryOwner{T}"/> to release accumulative tracking on dispose.
256+ /// </summary>
257+ private sealed class TrackingMemoryOwner < T > : IMemoryOwner < T >
258+ where T : struct
259+ {
260+ private IMemoryOwner < T > ? owner ;
261+ private readonly MemoryAllocator allocator ;
262+ private readonly long lengthInBytes ;
263+
264+ public TrackingMemoryOwner ( IMemoryOwner < T > owner , MemoryAllocator allocator , long lengthInBytes )
265+ {
266+ this . owner = owner ;
267+ this . allocator = allocator ;
268+ this . lengthInBytes = lengthInBytes ;
269+ }
270+
271+ public Memory < T > Memory => this . owner ? . Memory ?? Memory < T > . Empty ;
272+
273+ public void Dispose ( )
274+ {
275+ // Ensure only one caller disposes the inner owner and releases the reservation.
276+ IMemoryOwner < T > ? inner = Interlocked . Exchange ( ref this . owner , null ) ;
277+ if ( inner != null )
278+ {
279+ inner . Dispose ( ) ;
280+ this . allocator . ReleaseAccumulatedBytes ( this . lengthInBytes ) ;
281+ }
282+ }
283+ }
112284}
0 commit comments