Browse Source

Scale allocated chunk sizes to match allocations

pull/2006/head
James Jackson-South 4 years ago
parent
commit
fd4e3a5108
  1. 68
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  2. 70
      tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

68
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -17,9 +17,19 @@ namespace SixLabors.ImageSharp.IO
internal sealed class ChunkedMemoryStream : Stream
{
/// <summary>
/// The default length in bytes of each buffer chunk.
/// The default length in bytes of each buffer chunk when allocating large buffers.
/// </summary>
public const int DefaultBufferLength = 128 * 1024;
public const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb
/// <summary>
/// The threshold at which to switch to using the large buffer.
/// </summary>
public const int DefaultLargeChunkThreshold = DefaultLargeChunkSize / 4; // 1 Mb
/// <summary>
/// The default length in bytes of each buffer chunk when allocating small buffers.
/// </summary>
public const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb
// The memory allocator.
private readonly MemoryAllocator allocator;
@ -27,8 +37,18 @@ namespace SixLabors.ImageSharp.IO
// Data
private MemoryChunk memoryChunk;
// The length of each buffer chunk
private readonly int chunkLength;
// The length, in bytes, of each large buffer chunk
private readonly int largeChunkSize;
// The length, in bytes of the total allocation threshold that triggers switching from
// small to large buffer chunks.
private readonly int largeChunkThreshold;
// The current length, in bytes, of each buffer chunk
private int chunkSize;
// The total allocation length, in bytes
private int totalAllocation;
// Has the stream been disposed.
private bool isDisposed;
@ -49,21 +69,15 @@ namespace SixLabors.ImageSharp.IO
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
public ChunkedMemoryStream(MemoryAllocator allocator)
: this(DefaultBufferLength, allocator)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
/// <param name="bufferLength">The length, in bytes of each buffer chunk.</param>
/// <param name="allocator">The memory allocator.</param>
public ChunkedMemoryStream(int bufferLength, MemoryAllocator allocator)
{
Guard.MustBeGreaterThan(bufferLength, 0, nameof(bufferLength));
Guard.NotNull(allocator, nameof(allocator));
this.chunkLength = bufferLength;
// Tweak our buffer sizes to take the minimum of the provided buffer sizes
// or values scaled from the allocator buffer capacity which provides us with the largest
// available contiguous buffer size.
this.largeChunkSize = Math.Min(DefaultLargeChunkSize, allocator.GetBufferCapacityInBytes());
this.largeChunkThreshold = Math.Min(DefaultLargeChunkThreshold, this.largeChunkSize / 4);
this.chunkSize = Math.Min(DefaultSmallChunkSize, this.largeChunkSize / 32);
this.allocator = allocator;
}
@ -191,6 +205,9 @@ namespace SixLabors.ImageSharp.IO
case SeekOrigin.End:
this.Position = this.Length + offset;
break;
default:
ThrowInvalidSeek();
break;
}
return this.Position;
@ -219,6 +236,7 @@ namespace SixLabors.ImageSharp.IO
this.memoryChunk = null;
this.writeChunk = null;
this.readChunk = null;
this.totalAllocation = 0;
}
finally
{
@ -519,17 +537,25 @@ namespace SixLabors.ImageSharp.IO
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowDisposed()
=> throw new ObjectDisposedException(null, "The stream is closed.");
private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed.");
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRange(string value)
=> throw new ArgumentOutOfRangeException(value);
private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value);
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin.");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private MemoryChunk AllocateMemoryChunk()
{
IMemoryOwner<byte> buffer = this.allocator.Allocate<byte>(this.chunkLength);
if (this.totalAllocation >= this.largeChunkThreshold)
{
this.chunkSize = this.largeChunkSize;
}
IMemoryOwner<byte> buffer = this.allocator.Allocate<byte>(this.chunkSize);
this.totalAllocation += this.chunkSize;
return new MemoryChunk
{
Buffer = buffer,

70
tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

@ -20,17 +20,7 @@ namespace SixLabors.ImageSharp.Tests.IO
{
private readonly MemoryAllocator allocator;
public ChunkedMemoryStreamTests()
{
this.allocator = Configuration.Default.MemoryAllocator;
}
[Fact]
public void MemoryStream_Ctor_InvalidCapacities()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ChunkedMemoryStream(int.MinValue, this.allocator));
Assert.Throws<ArgumentOutOfRangeException>(() => new ChunkedMemoryStream(0, this.allocator));
}
public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator;
[Fact]
public void MemoryStream_GetPositionTest_Negative()
@ -61,11 +51,11 @@ namespace SixLabors.ImageSharp.Tests.IO
}
[Theory]
[InlineData(ChunkedMemoryStream.DefaultBufferLength)]
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))]
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)]
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))]
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize)]
[InlineData((int)(ChunkedMemoryStream.DefaultSmallChunkSize * 1.5))]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize * 4)]
[InlineData((int)(ChunkedMemoryStream.DefaultSmallChunkSize * 5.5))]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize * 16)]
public void MemoryStream_ReadByteTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
@ -73,7 +63,7 @@ namespace SixLabors.ImageSharp.Tests.IO
ms.CopyTo(cms);
cms.Position = 0;
var expected = ms.ToArray();
byte[] expected = ms.ToArray();
for (int i = 0; i < expected.Length; i++)
{
@ -82,11 +72,11 @@ namespace SixLabors.ImageSharp.Tests.IO
}
[Theory]
[InlineData(ChunkedMemoryStream.DefaultBufferLength)]
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))]
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)]
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))]
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize)]
[InlineData((int)(ChunkedMemoryStream.DefaultSmallChunkSize * 1.5))]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize * 4)]
[InlineData((int)(ChunkedMemoryStream.DefaultSmallChunkSize * 5.5))]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize * 16)]
public void MemoryStream_ReadByteBufferTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
@ -94,8 +84,8 @@ namespace SixLabors.ImageSharp.Tests.IO
ms.CopyTo(cms);
cms.Position = 0;
var expected = ms.ToArray();
var buffer = new byte[2];
byte[] expected = ms.ToArray();
byte[] buffer = new byte[2];
for (int i = 0; i < expected.Length; i += 2)
{
cms.Read(buffer);
@ -105,11 +95,11 @@ namespace SixLabors.ImageSharp.Tests.IO
}
[Theory]
[InlineData(ChunkedMemoryStream.DefaultBufferLength)]
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))]
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)]
[InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))]
[InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize)]
[InlineData((int)(ChunkedMemoryStream.DefaultSmallChunkSize * 1.5))]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize * 4)]
[InlineData((int)(ChunkedMemoryStream.DefaultSmallChunkSize * 5.5))]
[InlineData(ChunkedMemoryStream.DefaultSmallChunkSize * 16)]
public void MemoryStream_ReadByteBufferSpanTest(int length)
{
using MemoryStream ms = this.CreateTestStream(length);
@ -117,7 +107,7 @@ namespace SixLabors.ImageSharp.Tests.IO
ms.CopyTo(cms);
cms.Position = 0;
var expected = ms.ToArray();
byte[] expected = ms.ToArray();
Span<byte> buffer = new byte[2];
for (int i = 0; i < expected.Length; i += 2)
{
@ -257,24 +247,24 @@ namespace SixLabors.ImageSharp.Tests.IO
public void MemoryStream_CopyTo_Invalid()
{
ChunkedMemoryStream memoryStream;
const string BufferSize = "bufferSize";
const string bufferSize = nameof(bufferSize);
using (memoryStream = new ChunkedMemoryStream(this.allocator))
{
const string Destination = "destination";
Assert.Throws<ArgumentNullException>(Destination, () => memoryStream.CopyTo(destination: null));
const string destination = nameof(destination);
Assert.Throws<ArgumentNullException>(destination, () => memoryStream.CopyTo(destination: null));
// Validate the destination parameter first.
Assert.Throws<ArgumentNullException>(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0));
Assert.Throws<ArgumentNullException>(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1));
Assert.Throws<ArgumentNullException>(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0));
Assert.Throws<ArgumentNullException>(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1));
// Then bufferSize.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense.
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
}
// After the Stream is disposed, we should fail on all CopyTos.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated.
Assert.Throws<ArgumentOutOfRangeException>(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
ChunkedMemoryStream disposedStream = memoryStream;
@ -369,7 +359,7 @@ namespace SixLabors.ImageSharp.Tests.IO
private MemoryStream CreateTestStream(int length)
{
var buffer = new byte[length];
byte[] buffer = new byte[length];
var random = new Random();
random.NextBytes(buffer);

Loading…
Cancel
Save