// 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(); } }