// 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;
///
/// Verifies that a fully functional can be implemented outside the ImageSharp assembly.
///
public class MemoryAllocatorExtensibilityTests
{
private const int OneMegabyte = 1 << 20;
///
/// Verifies that an external allocator can apply from its constructor
/// and that the applied limits are readable through the public properties.
///
[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);
}
///
/// Verifies that the applied single buffer limit is capped to the group allocation limit.
///
[Fact]
public void ExternalAllocatorAppliedSingleBufferLimitIsCappedToGroupLimit()
{
ExternalArrayMemoryAllocator allocator = new(new MemoryAllocatorOptions
{
AllocationLimitMegabytes = 4,
SingleBufferAllocationLimitMegabytes = 8
});
Assert.Equal(4 * OneMegabyte, allocator.SingleBufferAllocationLimitBytes);
}
///
/// Verifies that an external allocator can set the limit properties directly
/// and that the base class validates allocations against the configured values.
///
[Fact]
public void ExternalAllocatorCanSetSingleBufferLimit()
{
ExternalArrayMemoryAllocator allocator = new();
allocator.SetLimits(
memoryGroupAllocationLimitBytes: 4096,
singleBufferAllocationLimitBytes: 1024,
accumulativeAllocationLimitBytes: 4096);
allocator.Allocate(1024).Dispose();
Assert.Throws(() => allocator.Allocate(1025));
}
///
/// Verifies that owners produced by an external allocator participate in accumulative allocation tracking.
///
[Fact]
public void ExternalAllocatorTracksAccumulativeAllocations()
{
ExternalArrayMemoryAllocator allocator = new();
allocator.SetLimits(
memoryGroupAllocationLimitBytes: 4096,
singleBufferAllocationLimitBytes: 4096,
accumulativeAllocationLimitBytes: 4096);
IMemoryOwner owner = allocator.Allocate(4096);
// The full accumulative budget is reserved while the owner is live.
Assert.Throws(() => allocator.Allocate(1));
// Disposing the owner releases the reservation.
owner.Dispose();
allocator.Allocate(4096).Dispose();
}
///
/// Verifies that an external allocator can back image creation through ,
/// for both discontiguous and contiguous buffer preferences, and that disposal reaches the external owners.
///
/// The contiguous buffer preference to apply.
[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 image = new(configuration, 16, 16, Color.Red.ToPixel()))
{
Assert.True(allocator.CreatedOwners > 0);
Assert.Equal(Color.Red.ToPixel(), image[8, 8]);
}
Assert.Equal(0, allocator.LiveOwners);
}
///
/// A implemented with only the public API surface, backed by managed arrays.
///
private sealed class ExternalArrayMemoryAllocator : MemoryAllocator
{
///
/// Initializes a new instance of the class with default limits.
///
public ExternalArrayMemoryAllocator()
{
}
///
/// Initializes a new instance of the class with custom limits.
///
/// The to apply.
public ExternalArrayMemoryAllocator(MemoryAllocatorOptions options) => this.ApplyOptions(options);
///
/// Gets the total number of owners created by this allocator.
///
public int CreatedOwners { get; private set; }
///
/// Gets the number of owners created by this allocator that are not yet disposed.
///
public int LiveOwners { get; private set; }
///
/// Sets the protected limit properties directly, as a derived allocator can.
///
/// The group allocation limit, in bytes.
/// The single buffer allocation limit, in bytes.
/// The accumulative allocation limit, in bytes.
public void SetLimits(
long memoryGroupAllocationLimitBytes,
int singleBufferAllocationLimitBytes,
long accumulativeAllocationLimitBytes)
{
this.MemoryGroupAllocationLimitBytes = memoryGroupAllocationLimitBytes;
this.SingleBufferAllocationLimitBytes = singleBufferAllocationLimitBytes;
this.AccumulativeAllocationLimitBytes = accumulativeAllocationLimitBytes;
}
///
protected override int GetBufferCapacityInBytes() => int.MaxValue;
///
protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None)
{
this.CreatedOwners++;
this.LiveOwners++;
return new ExternalArrayMemoryManager(new T[length], this);
}
///
/// Records the disposal of an owner created by this allocator.
///
internal void OnOwnerDisposed() => this.LiveOwners--;
}
///
/// An implemented with only the public API surface.
///
/// The element type.
private sealed class ExternalArrayMemoryManager : AllocationTrackedMemoryManager
where T : struct
{
private readonly T[] array;
private readonly ExternalArrayMemoryAllocator allocator;
///
/// Initializes a new instance of the class.
///
/// The array that backs this owner.
/// The allocator that created this owner.
public ExternalArrayMemoryManager(T[] array, ExternalArrayMemoryAllocator allocator)
{
this.array = array;
this.allocator = allocator;
}
///
public override Span GetSpan() => this.array;
///
public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException("Pinning is not required by these tests.");
///
public override void Unpin()
{
}
///
protected override void DisposeCore(bool disposing) => this.allocator.OnOwnerDisposed();
}
}