Skip to content

Commit 35e14e7

Browse files
Add AllocationTrackedMemoryManager and refactor allocators
1 parent c5624b5 commit 35e14e7

13 files changed

Lines changed: 223 additions & 183 deletions
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers;
5+
6+
namespace SixLabors.ImageSharp.Memory;
7+
8+
/// <summary>
9+
/// Provides the tracked memory-owner contract required by <see cref="MemoryAllocator"/>.
10+
/// </summary>
11+
/// <typeparam name="T">The element type.</typeparam>
12+
/// <remarks>
13+
/// Custom allocators implement <see cref="MemoryAllocator.AllocateCore{T}(int, AllocationOptions)"/>
14+
/// and return a derived type. The base allocator attaches allocation tracking after the owner has been
15+
/// created so custom implementations cannot forget, duplicate, or mismatch the reservation lifecycle.
16+
/// </remarks>
17+
public abstract class AllocationTrackedMemoryManager<T> : MemoryManager<T>
18+
where T : struct
19+
{
20+
private MemoryAllocator? trackingAllocator;
21+
private long trackingLengthInBytes;
22+
private int trackingReleased;
23+
24+
/// <summary>
25+
/// Releases resources held by the concrete tracked owner.
26+
/// </summary>
27+
/// <param name="disposing">
28+
/// <see langword="true"/> when the owner is being disposed deterministically;
29+
/// otherwise, <see langword="false"/>.
30+
/// </param>
31+
/// <remarks>
32+
/// Implementations release their own resources here. Allocation tracking is released by the sealed base
33+
/// dispose path after this method returns.
34+
/// </remarks>
35+
protected abstract void DisposeCore(bool disposing);
36+
37+
/// <inheritdoc />
38+
protected sealed override void Dispose(bool disposing)
39+
{
40+
this.DisposeCore(disposing);
41+
this.ReleaseAllocationTracking();
42+
}
43+
44+
/// <summary>
45+
/// Attaches allocation tracking to this owner after allocation has succeeded.
46+
/// </summary>
47+
/// <param name="allocator">The allocator that owns the reservation for this instance.</param>
48+
/// <param name="lengthInBytes">The reserved allocation size, in bytes.</param>
49+
/// <remarks>
50+
/// <see cref="MemoryAllocator"/> calls this exactly once after <c>AllocateCore</c> returns.
51+
/// Derived allocators should not call it themselves; they only construct the concrete owner.
52+
/// </remarks>
53+
internal void AttachAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
54+
{
55+
this.trackingAllocator = allocator;
56+
this.trackingLengthInBytes = lengthInBytes;
57+
}
58+
59+
/// <summary>
60+
/// Releases any tracked allocation bytes associated with this instance.
61+
/// </summary>
62+
/// <remarks>
63+
/// Calling this more than once is safe; only the first call after tracking has been attached releases bytes.
64+
/// </remarks>
65+
private void ReleaseAllocationTracking()
66+
{
67+
if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null)
68+
{
69+
this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes);
70+
this.trackingAllocator = null;
71+
}
72+
}
73+
}
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
namespace SixLabors.ImageSharp.Memory;
55

6+
/// <summary>
7+
/// Provides helper methods for working with <see cref="AllocationOptions"/>.
8+
/// </summary>
69
internal static class AllocationOptionsExtensions
710
{
8-
public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag;
11+
/// <summary>
12+
/// Returns a value indicating whether the specified flag is set on the allocation options.
13+
/// </summary>
14+
/// <param name="options">The allocation options to inspect.</param>
15+
/// <param name="flag">The flag to test for.</param>
16+
/// <returns><see langword="true"/> if <paramref name="flag"/> is set; otherwise, <see langword="false"/>.</returns>
17+
public static bool Has(this AllocationOptions options, AllocationOptions flag)
18+
=> (options & flag) == flag;
919
}

src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public BasicArrayBuffer(T[] array)
4747
public override Span<T> GetSpan() => this.Array.AsSpan(0, this.Length);
4848

4949
/// <inheritdoc />
50-
protected override void Dispose(bool disposing)
50+
protected override void DisposeCore(bool disposing)
5151
{
5252
}
5353

src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
1111
/// Provides a base class for <see cref="IMemoryOwner{T}"/> implementations by implementing pinning logic for <see cref="MemoryManager{T}"/> adaption.
1212
/// </summary>
1313
/// <typeparam name="T">The element type.</typeparam>
14-
internal abstract class ManagedBufferBase<T> : MemoryManager<T>
14+
internal abstract class ManagedBufferBase<T> : AllocationTrackedMemoryManager<T>
1515
where T : struct
1616
{
1717
private GCHandle pinHandle;

src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public SharedArrayPoolBuffer(int lengthInElements)
2424

2525
public byte[]? Array { get; private set; }
2626

27-
protected override void Dispose(bool disposing)
27+
protected override void DisposeCore(bool disposing)
2828
{
2929
if (this.Array == null)
3030
{

src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Memory.Internals;
1212
/// access to unmanaged buffers allocated by <see cref="Marshal.AllocHGlobal(int)"/>.
1313
/// </summary>
1414
/// <typeparam name="T">The element type.</typeparam>
15-
internal sealed unsafe class UnmanagedBuffer<T> : MemoryManager<T>, IRefCounted
15+
internal sealed unsafe class UnmanagedBuffer<T> : AllocationTrackedMemoryManager<T>, IRefCounted
1616
where T : struct
1717
{
1818
private readonly int lengthInElements;
@@ -52,7 +52,7 @@ public override MemoryHandle Pin(int elementIndex = 0)
5252
}
5353

5454
/// <inheritdoc />
55-
protected override void Dispose(bool disposing)
55+
protected override void DisposeCore(bool disposing)
5656
{
5757
DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!");
5858

src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

Lines changed: 91 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,11 @@ public abstract class MemoryAllocator
5555
internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte;
5656

5757
/// <summary>
58-
/// Gets a value indicating whether change tracking is currently suppressed for this instance.
58+
/// Gets a value indicating whether accumulative allocation tracking is currently suppressed for this instance.
5959
/// </summary>
6060
/// <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.
61+
/// This is used internally when an outer allocator or memory group reservation already owns the tracked bytes
62+
/// and nested allocations must not reserve or release them a second time.
6463
/// </remarks>
6564
private bool IsTrackingSuppressed => Volatile.Read(ref this.trackingSuppressionCount) > 0;
6665

@@ -105,9 +104,62 @@ public static MemoryAllocator Create(MemoryAllocatorOptions options)
105104
/// <param name="length">Size of the buffer to allocate.</param>
106105
/// <param name="options">The allocation options.</param>
107106
/// <returns>A buffer of values of type <typeparamref name="T"/>.</returns>
108-
/// <exception cref="ArgumentOutOfRangeException">When length is zero or negative.</exception>
107+
/// <exception cref="ArgumentOutOfRangeException">When length is negative.</exception>
109108
/// <exception cref="InvalidMemoryOperationException">When length is over the capacity of the allocator.</exception>
110-
public abstract IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
109+
public IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
110+
where T : struct
111+
{
112+
if (length < 0)
113+
{
114+
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
115+
}
116+
117+
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
118+
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
119+
{
120+
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
121+
}
122+
123+
long lengthInBytesLong = (long)lengthInBytes;
124+
bool shouldTrack = !this.IsTrackingSuppressed && lengthInBytesLong != 0;
125+
if (shouldTrack)
126+
{
127+
this.ReserveAllocation(lengthInBytesLong);
128+
}
129+
130+
try
131+
{
132+
AllocationTrackedMemoryManager<T> owner = this.AllocateCore<T>(length, options);
133+
if (shouldTrack)
134+
{
135+
owner.AttachAllocationTracking(this, lengthInBytesLong);
136+
}
137+
138+
return owner;
139+
}
140+
catch
141+
{
142+
if (shouldTrack)
143+
{
144+
this.ReleaseAccumulatedBytes(lengthInBytesLong);
145+
}
146+
147+
throw;
148+
}
149+
}
150+
151+
/// <summary>
152+
/// Allocates a tracked memory owner for <see cref="Allocate{T}(int, AllocationOptions)"/>.
153+
/// </summary>
154+
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
155+
/// <param name="length">Size of the buffer to allocate.</param>
156+
/// <param name="options">The allocation options.</param>
157+
/// <returns>A tracked memory owner of values of type <typeparamref name="T"/>.</returns>
158+
/// <remarks>
159+
/// Implementations should only allocate and initialize the concrete owner. The base allocator
160+
/// reserves bytes, attaches tracking to the returned owner, and releases the reservation if allocation fails.
161+
/// </remarks>
162+
protected abstract AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
111163
where T : struct;
112164

113165
/// <summary>
@@ -149,19 +201,31 @@ internal MemoryGroup<T> AllocateGroup<T>(
149201
}
150202

151203
long totalLengthInBytesLong = (long)totalLengthInBytes;
152-
this.ReserveAllocation(totalLengthInBytesLong);
204+
bool shouldTrack = !this.IsTrackingSuppressed && totalLengthInBytesLong != 0;
205+
if (shouldTrack)
206+
{
207+
this.ReserveAllocation(totalLengthInBytesLong);
208+
}
153209

154210
using (this.SuppressTracking())
155211
{
156212
try
157213
{
158214
MemoryGroup<T> group = this.AllocateGroupCore<T>(totalLength, totalLengthInBytesLong, bufferAlignment, options);
159-
group.SetAllocationTracking(this, totalLengthInBytesLong);
215+
if (shouldTrack)
216+
{
217+
group.AttachAllocationTracking(this, totalLengthInBytesLong);
218+
}
219+
160220
return group;
161221
}
162222
catch
163223
{
164-
this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
224+
if (shouldTrack)
225+
{
226+
this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
227+
}
228+
165229
throw;
166230
}
167231
}
@@ -172,28 +236,25 @@ internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements,
172236
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options);
173237

174238
/// <summary>
175-
/// Tracks the allocation of an <see cref="IMemoryOwner{T}" /> instance after reserving bytes.
239+
/// Allocates a single segment for <see cref="MemoryGroup{T}"/> construction.
176240
/// </summary>
177241
/// <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)
242+
/// <param name="length">Size of the segment to allocate.</param>
243+
/// <param name="options">The allocation options.</param>
244+
/// <returns>A segment owner for the requested buffer length.</returns>
245+
/// <remarks>
246+
/// The default implementation uses <see cref="Allocate{T}(int, AllocationOptions)"/>. Built-in allocators
247+
/// can override this to supply raw segment owners when group construction must bypass nested tracking.
248+
/// </remarks>
249+
internal virtual IMemoryOwner<T> AllocateGroupBuffer<T>(int length, AllocationOptions options = AllocationOptions.None)
182250
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-
}
251+
=> this.Allocate<T>(length, options);
191252

192253
/// <summary>
193254
/// Reserves accumulative allocation bytes before creating the underlying buffer.
194255
/// </summary>
195256
/// <param name="lengthInBytes">The number of bytes to reserve.</param>
196-
protected void ReserveAllocation(long lengthInBytes)
257+
private void ReserveAllocation(long lengthInBytes)
197258
{
198259
if (this.IsTrackingSuppressed || lengthInBytes <= 0)
199260
{
@@ -225,13 +286,17 @@ internal void ReleaseAccumulatedBytes(long lengthInBytes)
225286
/// <summary>
226287
/// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
227288
/// </summary>
228-
/// <returns>An <see cref="IDisposable"/> that restores tracking when disposed.</returns>
229-
internal IDisposable SuppressTracking() => new TrackingSuppressionScope(this);
289+
/// <returns>A scope that restores tracking when disposed.</returns>
290+
/// <remarks>
291+
/// Returning the concrete scope type keeps nested allocator calls allocation-free on the hot path
292+
/// while preserving the same using-pattern at call sites.
293+
/// </remarks>
294+
private TrackingSuppressionScope SuppressTracking() => new(this);
230295

231296
/// <summary>
232297
/// Temporarily suppresses accumulative allocation tracking within a scope.
233298
/// </summary>
234-
private sealed class TrackingSuppressionScope : IDisposable
299+
private struct TrackingSuppressionScope : IDisposable
235300
{
236301
private MemoryAllocator? allocator;
237302

@@ -250,35 +315,4 @@ public void Dispose()
250315
}
251316
}
252317
}
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-
}
284318
}

src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,31 +42,6 @@ public SimpleGcMemoryAllocator(MemoryAllocatorOptions options)
4242
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
4343

4444
/// <inheritdoc />
45-
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
46-
{
47-
if (length < 0)
48-
{
49-
InvalidMemoryOperationException.ThrowNegativeAllocationException(length);
50-
}
51-
52-
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
53-
if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
54-
{
55-
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
56-
}
57-
58-
long lengthInBytesLong = (long)lengthInBytes;
59-
this.ReserveAllocation(lengthInBytesLong);
60-
61-
try
62-
{
63-
IMemoryOwner<T> buffer = new BasicArrayBuffer<T>(new T[length]);
64-
return this.TrackAllocation(buffer, lengthInBytes);
65-
}
66-
catch
67-
{
68-
this.ReleaseAccumulatedBytes(lengthInBytesLong);
69-
throw;
70-
}
71-
}
45+
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None)
46+
=> new BasicArrayBuffer<T>(new T[length]);
7247
}

0 commit comments

Comments
 (0)