Browse Source

Merge pull request #3123 from SixLabors/js/revert-3120

Revert "Merge pull request #3056 from SixLabors/js/accumulative-memor…
pull/3125/head
James Jackson-South 4 weeks ago
committed by GitHub
parent
commit
10f749d5d8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 180
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  2. 30
      src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
  3. 40
      src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
  4. 58
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  5. 24
      src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
  6. 8
      src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
  7. 16
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
  8. 5
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  9. 38
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  10. 44
      tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
  11. 56
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  12. 10
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs

180
src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

@ -12,8 +12,6 @@ namespace SixLabors.ImageSharp.Memory;
public abstract class MemoryAllocator public abstract class MemoryAllocator
{ {
private const int OneGigabyte = 1 << 30; private const int OneGigabyte = 1 << 30;
private long accumulativeAllocatedBytes;
private int trackingSuppressionCount;
/// <summary> /// <summary>
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that /// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
@ -25,44 +23,9 @@ public abstract class MemoryAllocator
/// </summary> /// </summary>
public static MemoryAllocator Default { get; } = Create(); public static MemoryAllocator Default { get; } = Create();
/// <summary> 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.
/// </summary>
/// <remarks>
/// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and
/// 1 GB for 32-bit processes.
/// </remarks>
internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
/// <summary>
/// Gets the maximum allowed total allocation size, in bytes, for the current process.
/// </summary>
/// <remarks>
/// Defaults to <see cref="long.MaxValue"/>, 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.<br/>
/// When the cumulative 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;
/// <summary> internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
/// Gets the maximum size, in bytes, that can be allocated for a single buffer.
/// </summary>
/// <remarks>
/// The single buffer allocation limit is set to 1 GB by default.
/// </remarks>
internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte;
/// <summary>
/// Gets a value indicating whether change tracking is currently suppressed for this instance.
/// </summary>
/// <remarks>
/// When change tracking is suppressed, modifications to the object will not be recorded or
/// trigger change notifications. This property is used internally to temporarily disable tracking during
/// batch updates or initialization.
/// </remarks>
private bool IsTrackingSuppressed => Volatile.Read(ref this.trackingSuppressionCount) > 0;
/// <summary> /// <summary>
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes. /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@ -90,11 +53,6 @@ public abstract class MemoryAllocator
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes); allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
} }
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
allocator.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
return allocator; return allocator;
} }
@ -114,10 +72,6 @@ public abstract class MemoryAllocator
/// Releases all retained resources not being in use. /// Releases all retained resources not being in use.
/// Eg: by resetting array pools and letting GC to free the arrays. /// Eg: by resetting array pools and letting GC to free the arrays.
/// </summary> /// </summary>
/// <remarks>
/// This does not dispose active allocations; callers are responsible for disposing all
/// <see cref="IMemoryOwner{T}"/> instances to release memory.
/// </remarks>
public virtual void ReleaseRetainedResources() public virtual void ReleaseRetainedResources()
{ {
} }
@ -148,137 +102,11 @@ public abstract class MemoryAllocator
InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes); InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes);
} }
long totalLengthInBytesLong = (long)totalLengthInBytes; // Cast to long is safe because we already checked that the total length is within the limit.
this.ReserveAllocation(totalLengthInBytesLong); return this.AllocateGroupCore<T>(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
using (this.SuppressTracking())
{
try
{
MemoryGroup<T> group = this.AllocateGroupCore<T>(totalLength, totalLengthInBytesLong, bufferAlignment, options);
group.SetAllocationTracking(this, totalLengthInBytesLong);
return group;
}
catch
{
this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
throw;
}
}
} }
internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options) internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
where T : struct where T : struct
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options); => MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options);
/// <summary>
/// Tracks the allocation of an <see cref="IMemoryOwner{T}" /> instance after reserving bytes.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="owner">The allocation to track.</param>
/// <param name="lengthInBytes">The allocation size in bytes.</param>
/// <returns>The tracked allocation.</returns>
protected IMemoryOwner<T> TrackAllocation<T>(IMemoryOwner<T> owner, ulong lengthInBytes)
where T : struct
{
if (this.IsTrackingSuppressed || lengthInBytes == 0)
{
return owner;
}
return new TrackingMemoryOwner<T>(owner, this, (long)lengthInBytes);
}
/// <summary>
/// Reserves accumulative allocation bytes before creating the underlying buffer.
/// </summary>
/// <param name="lengthInBytes">The number of bytes to reserve.</param>
protected 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);
}
}
/// <summary>
/// Releases accumulative allocation bytes previously tracked by this allocator.
/// </summary>
/// <param name="lengthInBytes">The number of bytes to release.</param>
internal void ReleaseAccumulatedBytes(long lengthInBytes)
{
if (lengthInBytes <= 0)
{
return;
}
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
}
/// <summary>
/// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
/// </summary>
/// <returns>An <see cref="IDisposable"/> that restores tracking when disposed.</returns>
internal IDisposable SuppressTracking() => new TrackingSuppressionScope(this);
/// <summary>
/// Temporarily suppresses accumulative allocation tracking within a scope.
/// </summary>
private sealed class 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;
}
}
}
/// <summary>
/// Wraps an <see cref="IMemoryOwner{T}"/> to release accumulative tracking on dispose.
/// </summary>
private sealed class TrackingMemoryOwner<T> : IMemoryOwner<T>
where T : struct
{
private IMemoryOwner<T>? owner;
private readonly MemoryAllocator allocator;
private readonly long lengthInBytes;
public TrackingMemoryOwner(IMemoryOwner<T> owner, MemoryAllocator allocator, long lengthInBytes)
{
this.owner = owner;
this.allocator = allocator;
this.lengthInBytes = lengthInBytes;
}
public Memory<T> Memory => this.owner?.Memory ?? Memory<T>.Empty;
public void Dispose()
{
// Ensure only one caller disposes the inner owner and releases the reservation.
IMemoryOwner<T>? inner = Interlocked.Exchange(ref this.owner, null);
if (inner != null)
{
inner.Dispose();
this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes);
}
}
}
} }

30
src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs

@ -10,7 +10,6 @@ public struct MemoryAllocatorOptions
{ {
private int? maximumPoolSizeMegabytes; private int? maximumPoolSizeMegabytes;
private int? allocationLimitMegabytes; private int? allocationLimitMegabytes;
private int? accumulativeAllocationLimitMegabytes;
/// <summary> /// <summary>
/// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool /// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool
@ -18,7 +17,7 @@ public struct MemoryAllocatorOptions
/// </summary> /// </summary>
public int? MaximumPoolSizeMegabytes public int? MaximumPoolSizeMegabytes
{ {
readonly get => this.maximumPoolSizeMegabytes; get => this.maximumPoolSizeMegabytes;
set set
{ {
if (value.HasValue) if (value.HasValue)
@ -36,7 +35,7 @@ public struct MemoryAllocatorOptions
/// </summary> /// </summary>
public int? AllocationLimitMegabytes public int? AllocationLimitMegabytes
{ {
readonly get => this.allocationLimitMegabytes; get => this.allocationLimitMegabytes;
set set
{ {
if (value.HasValue) if (value.HasValue)
@ -47,29 +46,4 @@ public struct MemoryAllocatorOptions
this.allocationLimitMegabytes = value; this.allocationLimitMegabytes = value;
} }
} }
/// <summary>
/// Gets or sets a value defining the maximum total size that can be allocated by the allocator in Megabytes.
/// <see langword="null"/> means platform default: 2GB on 32-bit processes, 8GB on 64-bit processes.
/// </summary>
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;
}
}
} }

40
src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

@ -12,32 +12,6 @@ namespace SixLabors.ImageSharp.Memory;
/// </summary> /// </summary>
public sealed class SimpleGcMemoryAllocator : MemoryAllocator public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with default limits.
/// </summary>
public SimpleGcMemoryAllocator()
: this(default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with custom limits.
/// </summary>
/// <param name="options">The <see cref="MemoryAllocatorOptions"/> to apply.</param>
public SimpleGcMemoryAllocator(MemoryAllocatorOptions options)
{
if (options.AllocationLimitMegabytes.HasValue)
{
this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
}
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
}
/// <inheritdoc /> /// <inheritdoc />
protected internal override int GetBufferCapacityInBytes() => int.MaxValue; protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
@ -55,18 +29,6 @@ public sealed class SimpleGcMemoryAllocator : MemoryAllocator
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes); InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
} }
long lengthInBytesLong = (long)lengthInBytes; return new BasicArrayBuffer<T>(new T[length]);
this.ReserveAllocation(lengthInBytesLong);
try
{
IMemoryOwner<T> buffer = new BasicArrayBuffer<T>(new T[length]);
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
}
} }
} }

58
src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

@ -92,24 +92,13 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes) if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
{ {
long lengthInBytesLong = (long)lengthInBytes; SharedArrayPoolBuffer<T> buffer = new(length);
this.ReserveAllocation(lengthInBytesLong); if (options.Has(AllocationOptions.Clean))
try
{
SharedArrayPoolBuffer<T> buffer = new(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{ {
this.ReleaseAccumulatedBytes(lengthInBytesLong); buffer.GetSpan().Clear();
throw;
} }
return buffer;
} }
if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes) if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
@ -117,38 +106,12 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
UnmanagedMemoryHandle mem = this.pool.Rent(); UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid) if (mem.IsValid)
{ {
long lengthInBytesLong = (long)lengthInBytes; UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
this.ReserveAllocation(lengthInBytesLong); return buffer;
try
{
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
}
} }
} }
long nonPooledLengthInBytesLong = (long)lengthInBytes; return this.nonPoolAllocator.Allocate<T>(length, options);
this.ReserveAllocation(nonPooledLengthInBytesLong);
try
{
using (this.nonPoolAllocator.SuppressTracking())
{
IMemoryOwner<T> nonPooled = this.nonPoolAllocator.Allocate<T>(length, options);
return this.TrackAllocation(nonPooled, lengthInBytes);
}
}
catch
{
this.ReleaseAccumulatedBytes(nonPooledLengthInBytesLong);
throw;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -181,10 +144,7 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
return poolGroup; return poolGroup;
} }
using (this.nonPoolAllocator.SuppressTracking()) return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
{
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
}
} }
public override void ReleaseRetainedResources() => this.pool.Release(); public override void ReleaseRetainedResources() => this.pool.Release();

24
src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Memory;
@ -20,26 +19,13 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes; protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None) public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
{ {
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>(); UnmanagedBuffer<T> buffer = UnmanagedBuffer<T>.Allocate(length);
long lengthInBytesLong = (long)lengthInBytes; if (options.Has(AllocationOptions.Clean))
this.ReserveAllocation(lengthInBytesLong);
try
{ {
UnmanagedBuffer<T> buffer = UnmanagedBuffer<T>.Allocate(length); buffer.GetSpan().Clear();
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
} }
return buffer;
} }
} }

8
src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs

@ -15,12 +15,12 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer. /// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller. /// The last buffer is allowed to be smaller.
/// </summary> /// </summary>
public int BufferLength { get; } int BufferLength { get; }
/// <summary> /// <summary>
/// Gets the aggregate number of elements in the group. /// Gets the aggregate number of elements in the group.
/// </summary> /// </summary>
public long TotalLength { get; } long TotalLength { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the group has been invalidated. /// Gets a value indicating whether the group has been invalidated.
@ -29,7 +29,7 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces /// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces
/// the image buffers internally. /// the image buffers internally.
/// </remarks> /// </remarks>
public bool IsValid { get; } bool IsValid { get; }
/// <summary> /// <summary>
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current /// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
@ -39,5 +39,5 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// implementation, which is still available when casting to one of the underlying interfaces. /// implementation, which is still available when casting to one of the underlying interfaces.
/// </summary> /// </summary>
/// <returns>A new <see cref="MemoryGroupEnumerator{T}"/> instance mapping the current <see cref="Memory{T}"/> values in use.</returns> /// <returns>A new <see cref="MemoryGroupEnumerator{T}"/> instance mapping the current <see cref="Memory{T}"/> values in use.</returns>
public new MemoryGroupEnumerator<T> GetEnumerator(); new MemoryGroupEnumerator<T> GetEnumerator();
} }

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

@ -31,23 +31,23 @@ internal abstract partial class MemoryGroup<T>
/// <inheritdoc/> /// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator() => new(this); public override MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator() IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
/* The runtime sees the Array class as if it implemented the /* The runtime sees the Array class as if it implemented the
* type-generic collection interfaces explicitly, so here we * type-generic collection interfaces explicitly, so here we
* can just cast the source array to IList<Memory<T>> (or to * can just cast the source array to IList<Memory<T>> (or to
* an equivalent type), and invoke the generic GetEnumerator * an equivalent type), and invoke the generic GetEnumerator
* method directly from that interface reference. This saves * method directly from that interface reference. This saves
* having to create our own iterator block here. */ * having to create our own iterator block here. */
=> ((IList<Memory<T>>)this.source).GetEnumerator(); return ((IList<Memory<T>>)this.source).GetEnumerator();
public override void Dispose()
{
this.View.Invalidate();
this.ReleaseAllocationTracking();
} }
public override void Dispose() => this.View.Invalidate();
} }
} }

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

@ -73,8 +73,8 @@ internal abstract partial class MemoryGroup<T>
result[i] = currentBuffer; result[i] = currentBuffer;
} }
ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[^1], sizeOfLastBuffer, options); ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options);
result[^1] = lastBuffer; result[result.Length - 1] = lastBuffer;
return result; return result;
} }
@ -155,7 +155,6 @@ internal abstract partial class MemoryGroup<T>
} }
} }
this.ReleaseAllocationTracking();
this.memoryOwners = null; this.memoryOwners = null;
this.IsValid = false; this.IsValid = false;
this.groupLifetimeGuard = null; this.groupLifetimeGuard = null;

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

@ -22,9 +22,6 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
private static readonly int ElementSize = Unsafe.SizeOf<T>(); private static readonly int ElementSize = Unsafe.SizeOf<T>();
private MemoryGroupSpanCache memoryGroupSpanCache; private MemoryGroupSpanCache memoryGroupSpanCache;
private MemoryAllocator? trackingAllocator;
private long trackingLengthInBytes;
private int trackingReleased;
private MemoryGroup(int bufferLength, long totalLength) private MemoryGroup(int bufferLength, long totalLength)
{ {
@ -55,45 +52,16 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public abstract MemoryGroupEnumerator<T> GetEnumerator(); public abstract MemoryGroupEnumerator<T> GetEnumerator();
/// <summary>
/// Configures allocation tracking by specifying the allocator and the length, in bytes, to be tracked.
/// </summary>
/// <param name="allocator">The memory allocator to use for tracking allocations.</param>
/// <param name="lengthInBytes">The length, in bytes, of the memory region to track. Must be greater than or equal to zero.</param>
/// <remarks>
/// Intended for initialization; callers should avoid changing tracking state concurrently with disposal.
/// </remarks>
internal void SetAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
{
this.trackingAllocator = allocator;
this.trackingLengthInBytes = lengthInBytes;
}
/// <summary>
/// Releases any resources or tracking information associated with allocation tracking for this instance.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal void ReleaseAllocationTracking()
{
if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null)
{
this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes);
this.trackingAllocator = null;
}
}
/// <inheritdoc /> /// <inheritdoc />
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator() IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
/* This method is implemented in each derived class. /* This method is implemented in each derived class.
* Implementing the method here as non-abstract and throwing, * Implementing the method here as non-abstract and throwing,
* then reimplementing it explicitly in each derived class, is * then reimplementing it explicitly in each derived class, is
* a workaround for the lack of support for abstract explicit * a workaround for the lack of support for abstract explicit
* interface method implementations in C#. */ * interface method implementations in C#. */
=> throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()"); throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()");
}
/// <inheritdoc /> /// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();

44
tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs

@ -48,50 +48,6 @@ public class SimpleGcMemoryAllocatorTests
} }
} }
[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<byte> b0 = allocator.Allocate<byte>(oneMb);
// Additional allocation should exceed the limit while the owner is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1));
// Disposing the owner releases the reservation.
b0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.Allocate<byte>(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<byte> g0 = allocator.AllocateGroup<byte>(oneMb, 1024);
// Additional allocation should exceed the limit while the group is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(1, 1024));
// Disposing the group releases the reservation.
g0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
}
[StructLayout(LayoutKind.Explicit, Size = 512)] [StructLayout(LayoutKind.Explicit, Size = 512)]
private struct BigStruct private struct BigStruct
{ {

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

@ -16,8 +16,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
{ {
public class BufferTests1 : BufferTestSuite public class BufferTests1 : BufferTestSuite
{ {
private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() => private static MemoryAllocator CreateMemoryAllocator() =>
new( new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes: 1024, sharedArrayPoolThresholdInBytes: 1024,
poolBufferSizeInBytes: 2048, poolBufferSizeInBytes: 2048,
maxPoolSizeInBytes: 2048 * 4, maxPoolSizeInBytes: 2048 * 4,
@ -31,8 +31,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
public class BufferTests2 : BufferTestSuite public class BufferTests2 : BufferTestSuite
{ {
private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() => private static MemoryAllocator CreateMemoryAllocator() =>
new( new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes: 512, sharedArrayPoolThresholdInBytes: 512,
poolBufferSizeInBytes: 1024, poolBufferSizeInBytes: 1024,
maxPoolSizeInBytes: 1024 * 4, maxPoolSizeInBytes: 1024 * 4,
@ -179,8 +179,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
g1.Dispose(); g1.Dispose();
// Do some unmanaged allocations to make sure new non-pooled unmanaged allocations will grab different memory: // Do some unmanaged allocations to make sure new non-pooled unmanaged allocations will grab different memory:
IntPtr dummy1 = Marshal.AllocHGlobal(checked((IntPtr)B(8))); IntPtr dummy1 = Marshal.AllocHGlobal((IntPtr)B(8));
IntPtr dummy2 = Marshal.AllocHGlobal(checked((IntPtr)B(8))); IntPtr dummy2 = Marshal.AllocHGlobal((IntPtr)B(8));
using MemoryGroup<byte> g2 = allocator.AllocateGroup<byte>(B(8), 1024); using MemoryGroup<byte> g2 = allocator.AllocateGroup<byte>(B(8), 1024);
using MemoryGroup<byte> g3 = allocator.AllocateGroup<byte>(B(8), 1024); using MemoryGroup<byte> g3 = allocator.AllocateGroup<byte>(B(8), 1024);
@ -433,50 +433,6 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(5 * oneMb, 1024)); Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(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<byte> b0 = allocator.Allocate<byte>(oneMb);
// Additional allocation should exceed the limit while the owner is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1));
// Disposing the owner releases the reservation.
b0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.Allocate<byte>(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<byte> g0 = allocator.AllocateGroup<byte>(oneMb, 1024);
// Additional allocation should exceed the limit while the group is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(1, 1024));
// Disposing the group releases the reservation.
g0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
}
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))] [ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
public void MemoryAllocator_Create_SetHighLimit() public void MemoryAllocator_Create_SetHighLimit()
{ {

10
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs

@ -98,15 +98,7 @@ public partial class MemoryGroupTests
[InlineData(AllocationOptions.Clean)] [InlineData(AllocationOptions.Clean)]
public unsafe void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options) public unsafe void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options)
{ {
// Disable trimming to avoid buffers being freed between Return and TryAllocate by the UniformUnmanagedMemoryPool pool = new(10, 5);
// trim timer or the Gen2 GC callback.
UniformUnmanagedMemoryPool pool = new(
10,
5,
new UniformUnmanagedMemoryPool.TrimSettings
{
Rate = 0
});
UnmanagedMemoryHandle[] buffers = pool.Rent(5); UnmanagedMemoryHandle[] buffers = pool.Rent(5);
foreach (UnmanagedMemoryHandle b in buffers) foreach (UnmanagedMemoryHandle b in buffers)
{ {

Loading…
Cancel
Save