Skip to content

Commit 2beb01b

Browse files
Introduce ApplyOptions and use in allocators
1 parent 88acf75 commit 2beb01b

6 files changed

Lines changed: 61 additions & 19 deletions

File tree

src/ImageSharp/Memory/AllocationTrackingState.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ internal struct AllocationTrackingState
2121
/// </summary>
2222
/// <param name="allocator">The allocator that owns the reservation.</param>
2323
/// <param name="lengthInBytes">The reserved allocation size, in bytes.</param>
24+
/// <remarks>
25+
/// Must complete-before the owning object's reference is observable to any other thread.
26+
/// <see cref="MemoryAllocator"/> guarantees this by attaching synchronously on the allocating
27+
/// thread before returning the owner; reference publication then provides the release fence
28+
/// that makes these field writes visible to a subsequent <see cref="Release"/> on another thread.
29+
/// </remarks>
2430
internal void Attach(MemoryAllocator allocator, long lengthInBytes)
2531
{
2632
this.allocator = allocator;

src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,26 @@ public abstract class MemoryAllocator
8383
public static MemoryAllocator Create(MemoryAllocatorOptions options)
8484
{
8585
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
86+
allocator.ApplyOptions(options);
87+
return allocator;
88+
}
89+
90+
/// <summary>
91+
/// Applies the supplied <see cref="MemoryAllocatorOptions"/> to this instance.
92+
/// </summary>
93+
/// <param name="options">The options to apply. Properties left as <see langword="null"/> are ignored.</param>
94+
private protected void ApplyOptions(MemoryAllocatorOptions options)
95+
{
8696
if (options.AllocationLimitMegabytes.HasValue)
8797
{
88-
allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
89-
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
98+
this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
99+
this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
90100
}
91101

92102
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
93103
{
94-
allocator.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
104+
this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
95105
}
96-
97-
return allocator;
98106
}
99107

100108
/// <summary>

src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,7 @@ public SimpleGcMemoryAllocator()
2222
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with custom limits.
2323
/// </summary>
2424
/// <param name="options">The <see cref="MemoryAllocatorOptions"/> to apply.</param>
25-
public SimpleGcMemoryAllocator(MemoryAllocatorOptions options)
26-
{
27-
if (options.AllocationLimitMegabytes.HasValue)
28-
{
29-
this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
30-
this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
31-
}
32-
33-
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
34-
{
35-
this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
36-
}
37-
}
25+
public SimpleGcMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options);
3826

3927
/// <inheritdoc />
4028
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;

src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ internal UniformUnmanagedMemoryPoolMemoryAllocator(
7070
this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes);
7171
}
7272

73+
internal UniformUnmanagedMemoryPoolMemoryAllocator(
74+
int sharedArrayPoolThresholdInBytes,
75+
int poolBufferSizeInBytes,
76+
long maxPoolSizeInBytes,
77+
int unmanagedBufferSizeInBytes,
78+
MemoryAllocatorOptions options)
79+
: this(sharedArrayPoolThresholdInBytes, poolBufferSizeInBytes, maxPoolSizeInBytes, unmanagedBufferSizeInBytes)
80+
=> this.ApplyOptions(options);
81+
7382
/// <inheritdoc />
7483
protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes;
7584

src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ public static MemoryGroup<T> Allocate(
102102
int bufferAlignmentInElements,
103103
AllocationOptions options = AllocationOptions.None)
104104
{
105-
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
106105
Guard.NotNull(allocator, nameof(allocator));
106+
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
107107

108108
if (totalLengthInElements < 0)
109109
{

tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,37 @@ public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose()
477477
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
478478
}
479479

480+
[Fact]
481+
public void AllocateGroup_AccumulativeLimit_NonPoolFallback_TracksOncePerGroup()
482+
{
483+
// Configure the pool with zero capacity so multi-segment requests bypass both the
484+
// single-buffer-from-pool path and MemoryGroup<T>.TryAllocate(pool, ...) and fall
485+
// through to MemoryGroup<T>.Allocate(nonPoolAllocator, ...). The unmanaged segment
486+
// size is small enough that the request must span multiple segments, which is the
487+
// path where per-segment double-counting could regress.
488+
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(
489+
sharedArrayPoolThresholdInBytes: 64 * 1024,
490+
poolBufferSizeInBytes: 128 * 1024,
491+
maxPoolSizeInBytes: 0,
492+
unmanagedBufferSizeInBytes: 256 * 1024,
493+
new MemoryAllocatorOptions { AccumulativeAllocationLimitMegabytes = 1 });
494+
495+
// 768 KB exceeds the pool buffer size, so the request takes the multi-segment
496+
// non-pool fallback (three 256 KB segments). If tracking double-counted (group
497+
// plus each segment), reservation would be 768 KB + 768 KB = 1.5 MB and exceed
498+
// the 1 MB limit on allocation itself.
499+
MemoryGroup<byte> g = allocator.AllocateGroup<byte>(768 * 1024, 1024);
500+
Assert.True(g.Count > 1, "Test setup must exercise the multi-segment fallback path.");
501+
502+
// Reservation should be exactly 768 KB; another 512 KB would push to 1.25 MB and throw.
503+
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(512 * 1024));
504+
505+
g.Dispose();
506+
507+
// After disposal the reservation is fully released; a second equivalent group succeeds.
508+
allocator.AllocateGroup<byte>(768 * 1024, 1024).Dispose();
509+
}
510+
480511
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
481512
public void MemoryAllocator_Create_SetHighLimit()
482513
{

0 commit comments

Comments
 (0)