mirror of https://github.com/SixLabors/ImageSharp
Browse Source
The allocation limits on MemoryAllocator were internal with no public way to raise the 1 GB single-buffer default, and ApplyOptions was not callable from external subclasses, which blocked custom allocators. - Expose MemoryGroupAllocationLimitBytes, SingleBufferAllocationLimitBytes, and AccumulativeAllocationLimitBytes with validated protected setters. - Make ApplyOptions protected so derived allocators can accept options. - Add MemoryAllocatorOptions.SingleBufferAllocationLimitMegabytes to raise the contiguous buffer cap up to 2047 MB without a custom allocator. - Add public-api tests that implement a complete external allocator. - Add behavior tests for the new option and its validation.pull/3165/head
4 changed files with 341 additions and 10 deletions
@ -0,0 +1,210 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.PublicApi.Tests; |
|||
|
|||
/// <summary>
|
|||
/// Verifies that a fully functional <see cref="MemoryAllocator"/> can be implemented outside the ImageSharp assembly.
|
|||
/// </summary>
|
|||
public class MemoryAllocatorExtensibilityTests |
|||
{ |
|||
private const int OneMegabyte = 1 << 20; |
|||
|
|||
/// <summary>
|
|||
/// Verifies that an external allocator can apply <see cref="MemoryAllocatorOptions"/> from its constructor
|
|||
/// and that the applied limits are readable through the public properties.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ExternalAllocatorCanApplyOptionsFromConstructor() |
|||
{ |
|||
ExternalArrayMemoryAllocator allocator = new(new MemoryAllocatorOptions |
|||
{ |
|||
AllocationLimitMegabytes = 8, |
|||
SingleBufferAllocationLimitMegabytes = 2, |
|||
AccumulativeAllocationLimitMegabytes = 8 |
|||
}); |
|||
|
|||
Assert.Equal(8L * OneMegabyte, allocator.MemoryGroupAllocationLimitBytes); |
|||
Assert.Equal(2 * OneMegabyte, allocator.SingleBufferAllocationLimitBytes); |
|||
Assert.Equal(8L * OneMegabyte, allocator.AccumulativeAllocationLimitBytes); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that the applied single buffer limit is capped to the group allocation limit.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ExternalAllocatorAppliedSingleBufferLimitIsCappedToGroupLimit() |
|||
{ |
|||
ExternalArrayMemoryAllocator allocator = new(new MemoryAllocatorOptions |
|||
{ |
|||
AllocationLimitMegabytes = 4, |
|||
SingleBufferAllocationLimitMegabytes = 8 |
|||
}); |
|||
|
|||
Assert.Equal(4 * OneMegabyte, allocator.SingleBufferAllocationLimitBytes); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that an external allocator can set the limit properties directly
|
|||
/// and that the base class validates allocations against the configured values.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ExternalAllocatorCanSetSingleBufferLimit() |
|||
{ |
|||
ExternalArrayMemoryAllocator allocator = new(); |
|||
allocator.SetLimits( |
|||
memoryGroupAllocationLimitBytes: 4096, |
|||
singleBufferAllocationLimitBytes: 1024, |
|||
accumulativeAllocationLimitBytes: 4096); |
|||
|
|||
allocator.Allocate<byte>(1024).Dispose(); |
|||
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1025)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that owners produced by an external allocator participate in accumulative allocation tracking.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ExternalAllocatorTracksAccumulativeAllocations() |
|||
{ |
|||
ExternalArrayMemoryAllocator allocator = new(); |
|||
allocator.SetLimits( |
|||
memoryGroupAllocationLimitBytes: 4096, |
|||
singleBufferAllocationLimitBytes: 4096, |
|||
accumulativeAllocationLimitBytes: 4096); |
|||
|
|||
IMemoryOwner<byte> owner = allocator.Allocate<byte>(4096); |
|||
|
|||
// The full accumulative budget is reserved while the owner is live.
|
|||
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1)); |
|||
|
|||
// Disposing the owner releases the reservation.
|
|||
owner.Dispose(); |
|||
allocator.Allocate<byte>(4096).Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that an external allocator can back image creation through <see cref="Configuration"/>,
|
|||
/// for both discontiguous and contiguous buffer preferences, and that disposal reaches the external owners.
|
|||
/// </summary>
|
|||
/// <param name="preferContiguousImageBuffers">The contiguous buffer preference to apply.</param>
|
|||
[Theory] |
|||
[InlineData(false)] |
|||
[InlineData(true)] |
|||
public void ExternalAllocatorBacksImageCreation(bool preferContiguousImageBuffers) |
|||
{ |
|||
ExternalArrayMemoryAllocator allocator = new(); |
|||
Configuration configuration = Configuration.Default.Clone(); |
|||
configuration.MemoryAllocator = allocator; |
|||
configuration.PreferContiguousImageBuffers = preferContiguousImageBuffers; |
|||
|
|||
using (Image<Rgba32> image = new(configuration, 16, 16, Color.Red.ToPixel<Rgba32>())) |
|||
{ |
|||
Assert.True(allocator.CreatedOwners > 0); |
|||
Assert.Equal(Color.Red.ToPixel<Rgba32>(), image[8, 8]); |
|||
} |
|||
|
|||
Assert.Equal(0, allocator.LiveOwners); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A <see cref="MemoryAllocator"/> implemented with only the public API surface, backed by managed arrays.
|
|||
/// </summary>
|
|||
private sealed class ExternalArrayMemoryAllocator : MemoryAllocator |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExternalArrayMemoryAllocator"/> class with default limits.
|
|||
/// </summary>
|
|||
public ExternalArrayMemoryAllocator() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExternalArrayMemoryAllocator"/> class with custom limits.
|
|||
/// </summary>
|
|||
/// <param name="options">The <see cref="MemoryAllocatorOptions"/> to apply.</param>
|
|||
public ExternalArrayMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options); |
|||
|
|||
/// <summary>
|
|||
/// Gets the total number of owners created by this allocator.
|
|||
/// </summary>
|
|||
public int CreatedOwners { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of owners created by this allocator that are not yet disposed.
|
|||
/// </summary>
|
|||
public int LiveOwners { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Sets the protected limit properties directly, as a derived allocator can.
|
|||
/// </summary>
|
|||
/// <param name="memoryGroupAllocationLimitBytes">The group allocation limit, in bytes.</param>
|
|||
/// <param name="singleBufferAllocationLimitBytes">The single buffer allocation limit, in bytes.</param>
|
|||
/// <param name="accumulativeAllocationLimitBytes">The accumulative allocation limit, in bytes.</param>
|
|||
public void SetLimits( |
|||
long memoryGroupAllocationLimitBytes, |
|||
int singleBufferAllocationLimitBytes, |
|||
long accumulativeAllocationLimitBytes) |
|||
{ |
|||
this.MemoryGroupAllocationLimitBytes = memoryGroupAllocationLimitBytes; |
|||
this.SingleBufferAllocationLimitBytes = singleBufferAllocationLimitBytes; |
|||
this.AccumulativeAllocationLimitBytes = accumulativeAllocationLimitBytes; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override int GetBufferCapacityInBytes() => int.MaxValue; |
|||
|
|||
/// <inheritdoc />
|
|||
protected override AllocationTrackedMemoryManager<T> AllocateCore<T>(int length, AllocationOptions options = AllocationOptions.None) |
|||
{ |
|||
this.CreatedOwners++; |
|||
this.LiveOwners++; |
|||
return new ExternalArrayMemoryManager<T>(new T[length], this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Records the disposal of an owner created by this allocator.
|
|||
/// </summary>
|
|||
internal void OnOwnerDisposed() => this.LiveOwners--; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// An <see cref="AllocationTrackedMemoryManager{T}"/> implemented with only the public API surface.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The element type.</typeparam>
|
|||
private sealed class ExternalArrayMemoryManager<T> : AllocationTrackedMemoryManager<T> |
|||
where T : struct |
|||
{ |
|||
private readonly T[] array; |
|||
private readonly ExternalArrayMemoryAllocator allocator; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExternalArrayMemoryManager{T}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="array">The array that backs this owner.</param>
|
|||
/// <param name="allocator">The allocator that created this owner.</param>
|
|||
public ExternalArrayMemoryManager(T[] array, ExternalArrayMemoryAllocator allocator) |
|||
{ |
|||
this.array = array; |
|||
this.allocator = allocator; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override Span<T> GetSpan() => this.array; |
|||
|
|||
/// <inheritdoc />
|
|||
public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException("Pinning is not required by these tests."); |
|||
|
|||
/// <inheritdoc />
|
|||
public override void Unpin() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void DisposeCore(bool disposing) => this.allocator.OnOwnerDisposed(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue