diff --git a/src/ImageSharp/Memory/ArrayPoolMemoryManager.Buffer{T}.cs b/src/ImageSharp/Memory/ArrayPoolMemoryManager.Buffer{T}.cs index 5b0352010..78e275e8c 100644 --- a/src/ImageSharp/Memory/ArrayPoolMemoryManager.Buffer{T}.cs +++ b/src/ImageSharp/Memory/ArrayPoolMemoryManager.Buffer{T}.cs @@ -1,4 +1,8 @@ -using System; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; namespace SixLabors.ImageSharp.Memory { @@ -10,15 +14,15 @@ namespace SixLabors.ImageSharp.Memory private class Buffer : IBuffer where T : struct { - private readonly ArrayPoolMemoryManager memoryManager; - private readonly int length; - public Buffer(byte[] data, int length, ArrayPoolMemoryManager memoryManager) + private readonly ArrayPool sourcePool; + + public Buffer(byte[] data, int length, ArrayPool sourcePool) { - this.memoryManager = memoryManager; this.Data = data; this.length = length; + this.sourcePool = sourcePool; } protected byte[] Data { get; private set; } @@ -32,15 +36,15 @@ namespace SixLabors.ImageSharp.Memory return; } - this.memoryManager.pool.Return(this.Data); + this.sourcePool.Return(this.Data); this.Data = null; } } private class ManagedByteBuffer : Buffer, IManagedByteBuffer { - public ManagedByteBuffer(byte[] data, int length, ArrayPoolMemoryManager memoryManager) - : base(data, length, memoryManager) + public ManagedByteBuffer(byte[] data, int length, ArrayPool sourcePool) + : base(data, length, sourcePool) { } diff --git a/src/ImageSharp/Memory/ArrayPoolMemoryManager.cs b/src/ImageSharp/Memory/ArrayPoolMemoryManager.cs index 41ef84784..4f80b15ec 100644 --- a/src/ImageSharp/Memory/ArrayPoolMemoryManager.cs +++ b/src/ImageSharp/Memory/ArrayPoolMemoryManager.cs @@ -1,27 +1,46 @@ -using System.Buffers; +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Memory { + using Guard = SixLabors.Guard; + /// /// Implements by allocating memory from . /// public partial class ArrayPoolMemoryManager : MemoryManager { /// - /// Defines the default maximum size of pooled arrays. - /// Currently set to a value equivalent to 16 MegaPixels of an image. + /// The default value for: maximum size of pooled arrays in bytes. + /// Currently set to 32MB, which is equivalent to 8 megapixels of raw data. + /// + internal const int DefaultMaxPooledBufferSizeInBytes = 32 * 1024 * 1024; + + /// + /// The value for: The threshold to pool arrays in which has less buckets for memory safety. /// - public const int DefaultMaxSizeInBytes = 4096 * 4096 * 4; + private const int DefaultLargeBufferThresholdInBytes = 8 * 1024 * 1024; - private readonly ArrayPool pool; + /// + /// The for huge buffers, which is not kept clean. + /// + private ArrayPool largeArrayPool; + + /// + /// The for small-to-medium buffers which is not kept clean. + /// + private ArrayPool normalArrayPool; /// /// Initializes a new instance of the class. /// public ArrayPoolMemoryManager() - : this(DefaultMaxSizeInBytes) + : this(DefaultMaxPooledBufferSizeInBytes, DefaultLargeBufferThresholdInBytes) { } @@ -30,10 +49,40 @@ namespace SixLabors.ImageSharp.Memory /// /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. public ArrayPoolMemoryManager(int maxPoolSizeInBytes) + : this(maxPoolSizeInBytes, GetLargeBufferThresholdInBytes(maxPoolSizeInBytes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + /// The threshold to pool arrays in which has less buckets for memory safety. + public ArrayPoolMemoryManager(int maxPoolSizeInBytes, int largeBufferThresholdInBytes) { Guard.MustBeGreaterThan(maxPoolSizeInBytes, 0, nameof(maxPoolSizeInBytes)); + Guard.MustBeLessThanOrEqualTo(largeBufferThresholdInBytes, maxPoolSizeInBytes, nameof(largeBufferThresholdInBytes)); + + this.MaxPoolSizeInBytes = maxPoolSizeInBytes; + this.LargeBufferThresholdInBytes = largeBufferThresholdInBytes; - this.pool = ArrayPool.Create(maxPoolSizeInBytes, 50); + this.InitArrayPools(); + } + + /// + /// Gets the maximum size of pooled arrays in bytes. + /// + public int MaxPoolSizeInBytes { get; } + + /// + /// Gets the threshold to pool arrays in which has less buckets for memory safety. + /// + public int LargeBufferThresholdInBytes { get; } + + /// + public override void ReleaseRetainedResources() + { + this.InitArrayPools(); } /// @@ -42,8 +91,10 @@ namespace SixLabors.ImageSharp.Memory int itemSizeBytes = Unsafe.SizeOf(); int bufferSizeInBytes = length * itemSizeBytes; - byte[] byteBuffer = this.pool.Rent(bufferSizeInBytes); - var buffer = new Buffer(byteBuffer, length, this); + ArrayPool pool = this.GetArrayPool(bufferSizeInBytes); + byte[] byteArray = pool.Rent(bufferSizeInBytes); + + var buffer = new Buffer(byteArray, length, pool); if (clear) { buffer.Clear(); @@ -54,8 +105,10 @@ namespace SixLabors.ImageSharp.Memory internal override IManagedByteBuffer AllocateManagedByteBuffer(int length, bool clear) { - byte[] array = this.pool.Rent(length); - var buffer = new ManagedByteBuffer(array, length, this); + ArrayPool pool = this.GetArrayPool(length); + byte[] byteArray = pool.Rent(length); + + var buffer = new ManagedByteBuffer(byteArray, length, pool); if (clear) { buffer.Clear(); @@ -63,5 +116,21 @@ namespace SixLabors.ImageSharp.Memory return buffer; } + + private static int GetLargeBufferThresholdInBytes(int maxPoolSizeInBytes) + { + return maxPoolSizeInBytes / 4; + } + + private ArrayPool GetArrayPool(int bufferSizeInBytes) + { + return bufferSizeInBytes <= this.LargeBufferThresholdInBytes ? this.normalArrayPool : this.largeArrayPool; + } + + private void InitArrayPools() + { + this.largeArrayPool = ArrayPool.Create(this.MaxPoolSizeInBytes, 8); + this.normalArrayPool = ArrayPool.Create(this.LargeBufferThresholdInBytes, 24); + } } } \ No newline at end of file diff --git a/src/ImageSharp/Memory/BufferExtensions.cs b/src/ImageSharp/Memory/BufferExtensions.cs index 8975d3b45..b863dfc9a 100644 --- a/src/ImageSharp/Memory/BufferExtensions.cs +++ b/src/ImageSharp/Memory/BufferExtensions.cs @@ -1,3 +1,6 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + using System; using System.Runtime.CompilerServices; diff --git a/src/ImageSharp/Memory/MemoryManager.cs b/src/ImageSharp/Memory/MemoryManager.cs index 7318a313e..8e2df8cec 100644 --- a/src/ImageSharp/Memory/MemoryManager.cs +++ b/src/ImageSharp/Memory/MemoryManager.cs @@ -30,5 +30,13 @@ namespace SixLabors.ImageSharp.Memory { return new BasicArrayBuffer(new T[length]); } + + /// + /// Releases all retained resources not being in use. + /// Eg: by resetting array pools and letting GC to free the arrays. + /// + public virtual void ReleaseRetainedResources() + { + } } } diff --git a/tests/ImageSharp.Tests/Memory/ArrayPoolMemoryManagerTests.cs b/tests/ImageSharp.Tests/Memory/ArrayPoolMemoryManagerTests.cs index fcd4c1b3b..0bd243fda 100644 --- a/tests/ImageSharp.Tests/Memory/ArrayPoolMemoryManagerTests.cs +++ b/tests/ImageSharp.Tests/Memory/ArrayPoolMemoryManagerTests.cs @@ -4,6 +4,7 @@ // ReSharper disable InconsistentNaming namespace SixLabors.ImageSharp.Tests.Memory { + using System; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -15,8 +16,10 @@ namespace SixLabors.ImageSharp.Tests.Memory public class ArrayPoolMemoryManagerTests { private const int MaxPooledBufferSizeInBytes = 2048; - - private MemoryManager MemoryManager { get; } = new ArrayPoolMemoryManager(MaxPooledBufferSizeInBytes); + + private const int LargeBufferThresholdInBytes = MaxPooledBufferSizeInBytes / 2; + + private MemoryManager MemoryManager { get; } = new ArrayPoolMemoryManager(MaxPooledBufferSizeInBytes, LargeBufferThresholdInBytes); /// /// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location @@ -38,11 +41,36 @@ namespace SixLabors.ImageSharp.Tests.Memory public class BufferTests : BufferTestSuite { public BufferTests() - : base(new ArrayPoolMemoryManager(MaxPooledBufferSizeInBytes)) + : base(new ArrayPoolMemoryManager(MaxPooledBufferSizeInBytes, LargeBufferThresholdInBytes)) { } } + public class Constructor + { + [Fact] + public void WhenBothParametersPassedByUser() + { + var mgr = new ArrayPoolMemoryManager(1111, 666); + Assert.Equal(1111, mgr.MaxPoolSizeInBytes); + Assert.Equal(666, mgr.LargeBufferThresholdInBytes); + } + + [Fact] + public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdIsAutoCalculated() + { + var mgr = new ArrayPoolMemoryManager(5000); + Assert.Equal(5000, mgr.MaxPoolSizeInBytes); + Assert.True(mgr.LargeBufferThresholdInBytes < mgr.MaxPoolSizeInBytes); + } + + [Fact] + public void When_LargeBufferThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_Throws() + { + Assert.ThrowsAny(() => { new ArrayPoolMemoryManager(100, 200); }); + } + } + [StructLayout(LayoutKind.Explicit, Size = MaxPooledBufferSizeInBytes / 5)] struct LargeStruct { @@ -98,5 +126,40 @@ namespace SixLabors.ImageSharp.Tests.Memory Assert.Equal(expected, secondAlloc.Span[0]); } } + + [Fact] + public void ReleaseRetainedResources_ReplacesInnerArrayPool() + { + IBuffer buffer = this.MemoryManager.Allocate(32); + ref int ptrToPrev0 = ref buffer.Span.DangerousGetPinnableReference(); + buffer.Dispose(); + + this.MemoryManager.ReleaseRetainedResources(); + buffer = this.MemoryManager.Allocate(32); + + Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.DangerousGetPinnableReference())); + } + + [Fact] + public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed() + { + IBuffer buffer = this.MemoryManager.Allocate(32); + this.MemoryManager.ReleaseRetainedResources(); + buffer.Dispose(); + } + + [Fact] + public void AllocationOverLargeArrayThreshold_UsesDifferentPool() + { + int arrayLengthThreshold = LargeBufferThresholdInBytes / sizeof(int); + + IBuffer small = this.MemoryManager.Allocate(arrayLengthThreshold - 1); + ref int ptr2Small = ref small.DangerousGetPinnableReference(); + small.Dispose(); + + IBuffer large = this.MemoryManager.Allocate(arrayLengthThreshold + 1); + + Assert.False(Unsafe.AreSame(ref ptr2Small, ref large.DangerousGetPinnableReference())); + } } } \ No newline at end of file