Browse Source

Fix multi-buffer group tracking and enforce limits

pull/3120/head
James Jackson-South 2 months ago
parent
commit
23b312cf7b
  1. 6
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  2. 11
      src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
  3. 31
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  4. 2
      src/ImageSharp/Memory/InvalidMemoryOperationException.cs
  5. 26
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

6
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;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Defaults to <see cref="long.MaxValue"/>, effectively imposing no limit on the cumulative total.
/// Defaults to <see cref="long.MaxValue"/>, 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.<br/>
/// When the cumulative size of active allocations exceeds this limit, an <see cref="InvalidMemoryOperationException"/> will be thrown to
/// When the accumulative size of active allocations exceeds this limit, an <see cref="InvalidMemoryOperationException"/> will be thrown to
/// prevent further allocations and signal that the limit has been breached.
/// </remarks>
internal long AccumulativeAllocationLimitBytes { get; private protected set; } = long.MaxValue;

11
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
}
/// <summary>
/// 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 <see cref="MemoryAllocator"/> instance.
/// <see langword="null"/> (the default) imposes no limit on the cumulative total.
/// <see langword="null"/> (the default) imposes no limit on the accumulative total.
/// </summary>
public int? AccumulativeAllocationLimitMegabytes
{

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

@ -78,6 +78,37 @@ internal abstract partial class MemoryGroup<T>
return;
}
if (memoryOwners?.Length > 1)
{
foreach (IMemoryOwner<T> memoryOwner in memoryOwners)
{
if (memoryOwner is not AllocationTrackedMemoryManager<T>)
{
// 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<T>)memoryOwners[i];
long ownerLengthInBytes = (long)trackedOwner.Memory.Length * Unsafe.SizeOf<T>();
trackedOwner.AttachAllocationTracking(allocator, ownerLengthInBytes);
remainingLengthInBytes -= ownerLengthInBytes;
}
trackedOwner = (AllocationTrackedMemoryManager<T>)memoryOwners[lastOwnerIndex];
trackedOwner.AttachAllocationTracking(allocator, remainingLengthInBytes);
return;
}
base.AttachAllocationTracking(allocator, lengthInBytes);
}

2
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}.");
}

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

@ -558,6 +558,32 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
allocator.AllocateGroup<byte>(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<byte>(768 * 1024, 1024).Dispose();
}
}
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
public void MemoryAllocator_Create_SetHighLimit()
{

Loading…
Cancel
Save