From 545cf7d822bb64cbb503d4f00300e6a145217635 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 17 Apr 2020 19:05:42 +0100 Subject: [PATCH] BufferedReadStream WIP - MagickImage not working. --- .../Common/Extensions/StreamExtensions.cs | 27 +- .../Components/Decoder/HuffmanScanBuffer.cs | 6 +- .../Components/Decoder/HuffmanScanDecoder.cs | 7 +- .../Formats/Jpeg/JpegDecoderCore.cs | 131 ++++---- src/ImageSharp/IO/BufferedReadStream.cs | 301 ++++++++++++++++++ src/ImageSharp/IO/BufferedReadStream2.cs | 293 +++++++++++++++++ src/ImageSharp/Image.FromStream.cs | 23 +- .../Codecs/Jpeg/DoubleBufferedStreams.cs | 106 +++++- .../IO/BufferedReadStreamTests.cs | 211 ++++++++++++ 9 files changed, 998 insertions(+), 107 deletions(-) create mode 100644 src/ImageSharp/IO/BufferedReadStream.cs create mode 100644 src/ImageSharp/IO/BufferedReadStream2.cs create mode 100644 tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 5d8668257..e811543e3 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -2,11 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.IO; using SixLabors.ImageSharp.Memory; -#if !SUPPORTS_SPAN_STREAM -using System.Buffers; -#endif namespace SixLabors.ImageSharp { @@ -40,7 +38,7 @@ namespace SixLabors.ImageSharp /// Skips the number of bytes in the given stream. /// /// The stream. - /// The count. + /// A byte offset relative to the origin parameter. public static void Skip(this Stream stream, int count) { if (count < 1) @@ -51,21 +49,22 @@ namespace SixLabors.ImageSharp if (stream.CanSeek) { stream.Seek(count, SeekOrigin.Current); // Position += count; + return; } - else + + var buffer = ArrayPool.Shared.Rent(count); + while (count > 0) { - var foo = new byte[count]; - while (count > 0) + int bytesRead = stream.Read(buffer, 0, count); + if (bytesRead == 0) { - int bytesRead = stream.Read(foo, 0, count); - if (bytesRead == 0) - { - break; - } - - count -= bytesRead; + break; } + + count -= bytesRead; } + + ArrayPool.Shared.Return(buffer); } public static void Read(this Stream stream, IManagedByteBuffer buffer) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs index 34fe1aecb..8a027f2b6 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System.IO; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.IO; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal struct HuffmanScanBuffer { - private readonly DoubleBufferedStreamReader stream; + private readonly Stream stream; // The entropy encoded code buffer. private ulong data; @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // Whether there is no more good data to pull from the stream for the current mcu. private bool badData; - public HuffmanScanBuffer(DoubleBufferedStreamReader stream) + public HuffmanScanBuffer(Stream stream) { this.stream = stream; this.data = 0ul; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index fbb2b5272..742b2ab88 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -2,10 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { @@ -19,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private readonly JpegFrame frame; private readonly HuffmanTable[] dcHuffmanTables; private readonly HuffmanTable[] acHuffmanTables; - private readonly DoubleBufferedStreamReader stream; + private readonly Stream stream; private readonly JpegComponent[] components; // The restart interval. @@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The successive approximation bit high end. /// The successive approximation bit low end. public HuffmanScanDecoder( - DoubleBufferedStreamReader stream, + Stream stream, JpegFrame frame, HuffmanTable[] dcHuffmanTables, HuffmanTable[] acHuffmanTables, diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 951fec1d4..102e80b0a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -9,7 +9,6 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; @@ -129,11 +128,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public int BitsPerPixel => this.ComponentCount * this.Frame.Precision; - /// - /// Gets the input stream. - /// - public DoubleBufferedStreamReader InputStream { get; private set; } - /// /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// @@ -170,7 +164,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The buffer to read file markers to /// The input stream /// The - public static JpegFileMarker FindNextFileMarker(byte[] marker, DoubleBufferedStreamReader stream) + public static JpegFileMarker FindNextFileMarker(byte[] marker, Stream stream) { int value = stream.Read(marker, 0, 2); @@ -239,19 +233,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public void ParseStream(Stream stream, bool metadataOnly = false) { this.Metadata = new ImageMetadata(); - this.InputStream = new DoubleBufferedStreamReader(this.configuration.MemoryAllocator, stream); // Check for the Start Of Image marker. - this.InputStream.Read(this.markerBuffer, 0, 2); + stream.Read(this.markerBuffer, 0, 2); var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { JpegThrowHelper.ThrowImageFormatException("Missing SOI marker."); } - this.InputStream.Read(this.markerBuffer, 0, 2); + stream.Read(this.markerBuffer, 0, 2); byte marker = this.markerBuffer[1]; - fileMarker = new JpegFileMarker(marker, (int)this.InputStream.Position - 2); + fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); this.QuantizationTables = new Block8x8F[4]; // Only assign what we need @@ -270,20 +263,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (!fileMarker.Invalid) { // Get the marker length - int remaining = this.ReadUint16() - 2; + int remaining = this.ReadUint16(stream) - 2; switch (fileMarker.Marker) { case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - this.ProcessStartOfFrameMarker(remaining, fileMarker, metadataOnly); + this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, metadataOnly); break; case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(); + this.ProcessStartOfScanMarker(stream); break; } else @@ -297,41 +290,41 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (metadataOnly) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } else { - this.ProcessDefineHuffmanTablesMarker(remaining); + this.ProcessDefineHuffmanTablesMarker(stream, remaining); } break; case JpegConstants.Markers.DQT: - this.ProcessDefineQuantizationTablesMarker(remaining); + this.ProcessDefineQuantizationTablesMarker(stream, remaining); break; case JpegConstants.Markers.DRI: if (metadataOnly) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } else { - this.ProcessDefineRestartIntervalMarker(remaining); + this.ProcessDefineRestartIntervalMarker(stream, remaining); } break; case JpegConstants.Markers.APP0: - this.ProcessApplicationHeaderMarker(remaining); + this.ProcessApplicationHeaderMarker(stream, remaining); break; case JpegConstants.Markers.APP1: - this.ProcessApp1Marker(remaining); + this.ProcessApp1Marker(stream, remaining); break; case JpegConstants.Markers.APP2: - this.ProcessApp2Marker(remaining); + this.ProcessApp2Marker(stream, remaining); break; case JpegConstants.Markers.APP3: @@ -345,33 +338,31 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: case JpegConstants.Markers.APP13: - this.InputStream.Skip(remaining); + stream.Skip(remaining); break; case JpegConstants.Markers.APP14: - this.ProcessApp14Marker(remaining); + this.ProcessApp14Marker(stream, remaining); break; case JpegConstants.Markers.APP15: case JpegConstants.Markers.COM: - this.InputStream.Skip(remaining); + stream.Skip(remaining); break; } } // Read on. - fileMarker = FindNextFileMarker(this.markerBuffer, this.InputStream); + fileMarker = FindNextFileMarker(this.markerBuffer, stream); } } /// public void Dispose() { - this.InputStream?.Dispose(); this.Frame?.Dispose(); // Set large fields to null. - this.InputStream = null; this.Frame = null; this.dcHuffmanTables = null; this.acHuffmanTables = null; @@ -485,18 +476,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the application header containing the JFIF identifier plus extra data. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApplicationHeaderMarker(int remaining) + private void ProcessApplicationHeaderMarker(Stream stream, int remaining) { // We can only decode JFif identifiers. if (remaining < JFifMarker.Length) { // Skip the application header length - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } - this.InputStream.Read(this.temp, 0, JFifMarker.Length); + stream.Read(this.temp, 0, JFifMarker.Length); remaining -= JFifMarker.Length; JFifMarker.TryParse(this.temp, out this.jFif); @@ -504,26 +496,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // TODO: thumbnail if (remaining > 0) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } } /// /// Processes the App1 marker retrieving any stored metadata /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApp1Marker(int remaining) + private void ProcessApp1Marker(Stream stream, int remaining) { const int Exif00 = 6; if (remaining < Exif00 || this.IgnoreMetadata) { // Skip the application header length - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } var profile = new byte[remaining]; - this.InputStream.Read(profile, 0, remaining); + stream.Read(profile, 0, remaining); if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker)) { @@ -544,26 +537,27 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the App2 marker retrieving any stored ICC profile information /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApp2Marker(int remaining) + private void ProcessApp2Marker(Stream stream, int remaining) { // Length is 14 though we only need to check 12. const int Icclength = 14; if (remaining < Icclength || this.IgnoreMetadata) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } var identifier = new byte[Icclength]; - this.InputStream.Read(identifier, 0, Icclength); + stream.Read(identifier, 0, Icclength); remaining -= Icclength; // We have read it by this point if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) { this.isIcc = true; var profile = new byte[remaining]; - this.InputStream.Read(profile, 0, remaining); + stream.Read(profile, 0, remaining); if (this.iccData is null) { @@ -578,7 +572,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg else { // Not an ICC profile we can handle. Skip the remaining bytes so we can carry on and ignore this. - this.InputStream.Skip(remaining); + stream.Skip(remaining); } } @@ -586,42 +580,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes the application header containing the Adobe identifier /// which stores image encoding information for DCT filters. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessApp14Marker(int remaining) + private void ProcessApp14Marker(Stream stream, int remaining) { const int MarkerLength = AdobeMarker.Length; if (remaining < MarkerLength) { // Skip the application header length - this.InputStream.Skip(remaining); + stream.Skip(remaining); return; } - this.InputStream.Read(this.temp, 0, MarkerLength); + stream.Read(this.temp, 0, MarkerLength); remaining -= MarkerLength; AdobeMarker.TryParse(this.temp, out this.adobe); if (remaining > 0) { - this.InputStream.Skip(remaining); + stream.Skip(remaining); } } /// /// Processes the Define Quantization Marker and tables. Specified in section B.2.4.1. /// + /// The input stream. /// The remaining bytes in the segment block. /// /// Thrown if the tables do not match the header /// - private void ProcessDefineQuantizationTablesMarker(int remaining) + private void ProcessDefineQuantizationTablesMarker(Stream stream, int remaining) { while (remaining > 0) { bool done = false; remaining--; - int quantizationTableSpec = this.InputStream.ReadByte(); + int quantizationTableSpec = stream.ReadByte(); int tableIndex = quantizationTableSpec & 15; // Max index. 4 Tables max. @@ -641,7 +637,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; } - this.InputStream.Read(this.temp, 0, 64); + stream.Read(this.temp, 0, 64); remaining -= 64; ref Block8x8F table = ref this.QuantizationTables[tableIndex]; @@ -661,7 +657,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg break; } - this.InputStream.Read(this.temp, 0, 128); + stream.Read(this.temp, 0, 128); remaining -= 128; ref Block8x8F table = ref this.QuantizationTables[tableIndex]; @@ -697,10 +693,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Processes the Start of Frame marker. Specified in section B.2.2. /// + /// The input stream. /// The remaining bytes in the segment block. /// The current frame marker. /// Whether to parse metadata only - private void ProcessStartOfFrameMarker(int remaining, in JpegFileMarker frameMarker, bool metadataOnly) + private void ProcessStartOfFrameMarker(Stream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly) { if (this.Frame != null) { @@ -709,7 +706,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Read initial marker definitions. const int length = 6; - this.InputStream.Read(this.temp, 0, length); + stream.Read(this.temp, 0, length); // We only support 8-bit and 12-bit precision. if (Array.IndexOf(this.supportedPrecisions, this.temp[0]) == -1) @@ -747,7 +744,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowBadMarker("SOFn", remaining); } - this.InputStream.Read(this.temp, 0, remaining); + stream.Read(this.temp, 0, remaining); // No need to pool this. They max out at 4 this.Frame.ComponentIds = new byte[this.ComponentCount]; @@ -794,8 +791,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes a Define Huffman Table marker, and initializes a huffman /// struct from its contents. Specified in section B.2.4.2. /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessDefineHuffmanTablesMarker(int remaining) + private void ProcessDefineHuffmanTablesMarker(Stream stream, int remaining) { int length = remaining; @@ -804,7 +802,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanData.GetSpan()); for (int i = 2; i < remaining;) { - byte huffmanTableSpec = (byte)this.InputStream.ReadByte(); + byte huffmanTableSpec = (byte)stream.ReadByte(); int tableType = huffmanTableSpec >> 4; int tableIndex = huffmanTableSpec & 15; @@ -820,7 +818,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowImageFormatException("Bad Huffman Table index."); } - this.InputStream.Read(huffmanData.Array, 0, 16); + stream.Read(huffmanData.Array, 0, 16); using (IManagedByteBuffer codeLengths = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(17, AllocationOptions.Clean)) { @@ -841,7 +839,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using (IManagedByteBuffer huffmanValues = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(256, AllocationOptions.Clean)) { - this.InputStream.Read(huffmanValues.Array, 0, codeLengthSum); + stream.Read(huffmanValues.Array, 0, codeLengthSum); i += 17 + codeLengthSum; @@ -860,32 +858,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in /// macroblocks /// + /// The input stream. /// The remaining bytes in the segment block. - private void ProcessDefineRestartIntervalMarker(int remaining) + private void ProcessDefineRestartIntervalMarker(Stream stream, int remaining) { if (remaining != 2) { JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining); } - this.resetInterval = this.ReadUint16(); + this.resetInterval = this.ReadUint16(stream); } /// /// Processes the SOS (Start of scan marker). /// - private void ProcessStartOfScanMarker() + /// The input stream. + private void ProcessStartOfScanMarker(Stream stream) { if (this.Frame is null) { JpegThrowHelper.ThrowImageFormatException("No readable SOFn (Start Of Frame) marker found."); } - int selectorsCount = this.InputStream.ReadByte(); + int selectorsCount = stream.ReadByte(); for (int i = 0; i < selectorsCount; i++) { int componentIndex = -1; - int selector = this.InputStream.ReadByte(); + int selector = stream.ReadByte(); for (int j = 0; j < this.Frame.ComponentIds.Length; j++) { @@ -903,20 +903,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } ref JpegComponent component = ref this.Frame.Components[componentIndex]; - int tableSpec = this.InputStream.ReadByte(); + int tableSpec = stream.ReadByte(); component.DCHuffmanTableId = tableSpec >> 4; component.ACHuffmanTableId = tableSpec & 15; this.Frame.ComponentOrder[i] = (byte)componentIndex; } - this.InputStream.Read(this.temp, 0, 3); + stream.Read(this.temp, 0, 3); int spectralStart = this.temp[0]; int spectralEnd = this.temp[1]; int successiveApproximation = this.temp[2]; var sd = new HuffmanScanDecoder( - this.InputStream, + stream, this.Frame, this.dcHuffmanTables, this.acHuffmanTables, @@ -944,11 +944,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Reads a from the stream advancing it by two bytes /// + /// The input stream. /// The [MethodImpl(InliningOptions.ShortMethod)] - private ushort ReadUint16() + private ushort ReadUint16(Stream stream) { - this.InputStream.Read(this.markerBuffer, 0, 2); + stream.Read(this.markerBuffer, 0, 2); return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs new file mode 100644 index 000000000..776fb123f --- /dev/null +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -0,0 +1,301 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.IO +{ + /// + /// A readonly stream that add a secondary level buffer in addition to native stream + /// buffered reading to reduce the overhead of small incremental reads. + /// + internal sealed unsafe class BufferedReadStream : Stream + { + /// + /// The length, in bytes, of the underlying buffer. + /// + public const int BufferLength = 8192; + + private const int MaxBufferIndex = BufferLength - 1; + + private readonly Stream stream; + + private readonly int streamLength; + + private readonly byte[] readBuffer; + + private MemoryHandle readBufferHandle; + + private readonly byte* pinnedReadBuffer; + + // Index within our buffer, not reader position. + private int readBufferIndex; + + // Matches what the stream position would be without buffering + private int readerPosition; + + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + public BufferedReadStream(Stream stream) + { + Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); + Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); + + // Ensure all underlying buffers have been flushed before we attempt to read the stream. + // User streams may have opted to throw from Flush if CanWrite is false + // (although the abstract Stream does not do so). + if (stream.CanWrite) + { + stream.Flush(); + } + + this.stream = stream; + this.Position = (int)stream.Position; + this.streamLength = (int)stream.Length; + + this.readBuffer = ArrayPool.Shared.Rent(BufferLength); + this.readBufferHandle = new Memory(this.readBuffer).Pin(); + this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; + + // This triggers a full read on first attempt. + this.readBufferIndex = BufferLength; + } + + /// + /// Gets the length, in bytes, of the stream. + /// + public override long Length => this.streamLength; + + /// + /// Gets or sets the current position within the stream. + /// + public override long Position + { + get => this.readerPosition; + + set + { + // Only reset readBufferIndex if we are out of bounds of our working buffer + // otherwise we should simply move the value by the diff. + int v = (int)value; + if (this.IsInReadBuffer(v, out int index)) + { + this.readBufferIndex = index; + this.readerPosition = v; + } + else + { + this.readerPosition = v; + this.stream.Seek(value, SeekOrigin.Begin); + this.readBufferIndex = BufferLength; + } + } + } + + /// + public override bool CanRead { get; } = true; + + /// + public override bool CanSeek { get; } = true; + + /// + public override bool CanWrite { get; } = false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int ReadByte() + { + if (this.readerPosition >= this.streamLength) + { + return -1; + } + + // Our buffer has been read. + // We need to refill and start again. + if (this.readBufferIndex > MaxBufferIndex) + { + this.FillReadBuffer(); + } + + this.readerPosition++; + return this.pinnedReadBuffer[this.readBufferIndex++]; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int Read(byte[] buffer, int offset, int count) + { + // Too big for our buffer. Read directly from the stream. + if (count > BufferLength) + { + return this.ReadToBufferDirectSlow(buffer, offset, count); + } + + // Too big for remaining buffer but less than entire buffer length + // Copy to buffer then read from there. + if (count + this.readBufferIndex > BufferLength) + { + return this.ReadToBufferViaCopySlow(buffer, offset, count); + } + + return this.ReadToBufferViaCopyFast(buffer, offset, count); + } + + /// + public override void Flush() + { + // Reset the stream position to match reader position. + if (this.readerPosition != this.stream.Position) + { + this.stream.Seek(this.readerPosition, SeekOrigin.Begin); + this.readerPosition = (int)this.stream.Position; + } + + // Reset to trigger full read on next attempt. + this.readBufferIndex = BufferLength; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + this.Position = offset; + } + else + { + this.Position += offset; + } + + return this.readerPosition; + } + + /// + /// This operation is not supported in . + public override void SetLength(long value) + => throw new NotSupportedException(); + + /// + /// This operation is not supported in . + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.readBufferHandle.Dispose(); + ArrayPool.Shared.Return(this.readBuffer); + this.Flush(); + + base.Dispose(true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetPositionDifference(int p) => p - this.readerPosition; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsInReadBuffer(int p, out int index) + { + index = this.GetPositionDifference(p) + this.readBufferIndex; + return index > -1 && index < BufferLength; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void FillReadBuffer() + { + if (this.readerPosition != this.stream.Position) + { + this.stream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + this.stream.Read(this.readBuffer, 0, BufferLength); + this.readBufferIndex = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) + { + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.readerPosition += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + return this.ReadToBufferViaCopyFast(buffer, offset, count); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) + { + // Read to target but don't copy to our read buffer. + if (this.readerPosition != this.stream.Position) + { + this.stream.Seek(this.readerPosition, SeekOrigin.Begin); + } + + int n = this.stream.Read(buffer, offset, count); + this.Position += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetCopyCount(int count) + { + int n = this.streamLength - this.readerPosition; + if (n > count) + { + n = count; + } + + if (n < 0) + { + n = 0; + } + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyBytes(byte[] buffer, int offset, int count) + { + // Same as MemoryStream. + if (count < 9) + { + int byteCount = count; + int read = this.readBufferIndex; + byte* pinned = this.pinnedReadBuffer; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = pinned[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); + } + } + } +} diff --git a/src/ImageSharp/IO/BufferedReadStream2.cs b/src/ImageSharp/IO/BufferedReadStream2.cs new file mode 100644 index 000000000..a35804ce2 --- /dev/null +++ b/src/ImageSharp/IO/BufferedReadStream2.cs @@ -0,0 +1,293 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.IO +{ + /// + /// A readonly stream that add a secondary level buffer in addition to native stream + /// buffered reading to reduce the overhead of small incremental reads. + /// + internal sealed unsafe class BufferedReadStream2 : IDisposable + { + /// + /// The length, in bytes, of the underlying buffer. + /// + public const int BufferLength = 8192; + + private const int MaxBufferIndex = BufferLength - 1; + + private readonly Stream stream; + + private readonly byte[] readBuffer; + + private MemoryHandle readBufferHandle; + + private readonly byte* pinnedReadBuffer; + + private int readBufferIndex; + + private readonly int length; + + private int position; + + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + public BufferedReadStream2(Stream stream) + { + Guard.IsTrue(stream.CanRead, nameof(stream), "Stream must be readable."); + Guard.IsTrue(stream.CanSeek, nameof(stream), "Stream must be seekable."); + + // Ensure all underlying buffers have been flushed before we attempt to read the stream. + // User streams may have opted to throw from Flush if CanWrite is false + // (although the abstract Stream does not do so). + if (stream.CanWrite) + { + stream.Flush(); + } + + this.stream = stream; + this.Position = (int)stream.Position; + this.length = (int)stream.Length; + + this.readBuffer = ArrayPool.Shared.Rent(BufferLength); + this.readBufferHandle = new Memory(this.readBuffer).Pin(); + this.pinnedReadBuffer = (byte*)this.readBufferHandle.Pointer; + + // This triggers a full read on first attempt. + this.readBufferIndex = BufferLength; + } + + /// + /// Gets the length, in bytes, of the stream. + /// + public long Length => this.length; + + /// + /// Gets or sets the current position within the stream. + /// + public long Position + { + get => this.position; + + set + { + // Only reset readIndex if we are out of bounds of our working buffer + // otherwise we should simply move the value by the diff. + int v = (int)value; + if (this.IsInReadBuffer(v, out int index)) + { + this.readBufferIndex = index; + this.position = v; + } + else + { + this.position = v; + this.stream.Seek(value, SeekOrigin.Begin); + this.readBufferIndex = BufferLength; + } + } + } + + public bool CanRead { get; } = true; + + public bool CanSeek { get; } = true; + + public bool CanWrite { get; } = false; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadByte() + { + if (this.position >= this.length) + { + return -1; + } + + if (this.readBufferIndex > MaxBufferIndex) + { + this.FillReadBuffer(); + } + + this.position++; + return this.pinnedReadBuffer[this.readBufferIndex++]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Read(byte[] buffer, int offset, int count) + { + if (count > BufferLength) + { + return this.ReadToBufferDirectSlow(buffer, offset, count); + } + + if (count + this.readBufferIndex > BufferLength) + { + return this.ReadToBufferViaCopySlow(buffer, offset, count); + } + + // return this.ReadToBufferViaCopyFast(buffer, offset, count); + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.position += n; + this.readBufferIndex += n; + + return n; + } + + public void Flush() + { + // Reset the stream position. + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + this.position = (int)this.stream.Position; + } + + this.readBufferIndex = BufferLength; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + this.Position = offset; + } + else + { + this.Position += offset; + } + + return this.position; + } + + public void SetLength(long value) + => throw new NotSupportedException(); + + public void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.readBufferHandle.Dispose(); + ArrayPool.Shared.Return(this.readBuffer); + this.Flush(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetPositionDifference(int p) => p - this.position; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsInReadBuffer(int p, out int index) + { + index = this.GetPositionDifference(p) + this.readBufferIndex; + return index > -1 && index < BufferLength; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void FillReadBuffer() + { + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + this.stream.Read(this.readBuffer, 0, BufferLength); + this.readBufferIndex = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadToBufferViaCopyFast(byte[] buffer, int offset, int count) + { + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.position += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferViaCopySlow(byte[] buffer, int offset, int count) + { + // Refill our buffer then copy. + this.FillReadBuffer(); + + // return this.ReadToBufferViaCopyFast(buffer, offset, count); + int n = this.GetCopyCount(count); + this.CopyBytes(buffer, offset, n); + + this.position += n; + this.readBufferIndex += n; + + return n; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private int ReadToBufferDirectSlow(byte[] buffer, int offset, int count) + { + // Read to target but don't copy to our read buffer. + if (this.position != this.stream.Position) + { + this.stream.Seek(this.position, SeekOrigin.Begin); + } + + int n = this.stream.Read(buffer, offset, count); + this.Position += n; + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetCopyCount(int count) + { + int n = this.length - this.position; + if (n > count) + { + n = count; + } + + if (n < 0) + { + n = 0; + } + + return n; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyBytes(byte[] buffer, int offset, int count) + { + // Same as MemoryStream. + if (count < 9) + { + int byteCount = count; + int read = this.readBufferIndex; + byte* pinned = this.pinnedReadBuffer; + + while (--byteCount > -1) + { + buffer[offset + byteCount] = pinned[read + byteCount]; + } + } + else + { + Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); + } + } + } +} diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index 52d71409b..7ef9038c5 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp @@ -31,7 +32,7 @@ namespace SixLabors.ImageSharp /// Thrown if the stream is not readable. /// The format type or null if none found. public static IImageFormat DetectFormat(Configuration configuration, Stream stream) - => WithSeekableStream(configuration, stream, s => InternalDetectFormat(s, configuration)); + => WithSeekableStream(configuration, stream, false, s => InternalDetectFormat(s, configuration)); /// /// Reads the raw image information from the specified stream without fully decoding it. @@ -66,7 +67,7 @@ namespace SixLabors.ImageSharp /// public static IImageInfo Identify(Configuration configuration, Stream stream, out IImageFormat format) { - (IImageInfo info, IImageFormat format) data = WithSeekableStream(configuration, stream, s => InternalIdentity(s, configuration ?? Configuration.Default)); + (IImageInfo info, IImageFormat format) data = WithSeekableStream(configuration, stream, false, s => InternalIdentity(s, configuration ?? Configuration.Default)); format = data.format; return data.info; @@ -115,7 +116,7 @@ namespace SixLabors.ImageSharp /// Image cannot be loaded. /// A new .> public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) => - WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + WithSeekableStream(configuration, stream, true, s => decoder.Decode(configuration, s)); /// /// Decode a new instance of the class from the given stream. @@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp /// A new .> public static Image Load(Stream stream, IImageDecoder decoder) where TPixel : unmanaged, IPixel - => WithSeekableStream(Configuration.Default, stream, s => decoder.Decode(Configuration.Default, s)); + => WithSeekableStream(Configuration.Default, stream, true, s => decoder.Decode(Configuration.Default, s)); /// /// Create a new instance of the class from the given stream. @@ -177,7 +178,7 @@ namespace SixLabors.ImageSharp /// A new .> public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) where TPixel : unmanaged, IPixel - => WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + => WithSeekableStream(configuration, stream, true, s => decoder.Decode(configuration, s)); /// /// Create a new instance of the class from the given stream. @@ -206,7 +207,7 @@ namespace SixLabors.ImageSharp where TPixel : unmanaged, IPixel { Guard.NotNull(configuration, nameof(configuration)); - (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration)); + (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, true, s => Decode(s, configuration)); format = data.format; @@ -239,7 +240,7 @@ namespace SixLabors.ImageSharp public static Image Load(Configuration configuration, Stream stream, out IImageFormat format) { Guard.NotNull(configuration, nameof(configuration)); - (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, s => Decode(s, configuration)); + (Image img, IImageFormat format) data = WithSeekableStream(configuration, stream, true, s => Decode(s, configuration)); format = data.format; @@ -259,7 +260,7 @@ namespace SixLabors.ImageSharp throw new UnknownImageFormatException(sb.ToString()); } - private static T WithSeekableStream(Configuration configuration, Stream stream, Func action) + private static T WithSeekableStream(Configuration configuration, Stream stream, bool buffer, Func action) { if (!stream.CanRead) { @@ -273,6 +274,12 @@ namespace SixLabors.ImageSharp stream.Position = 0; } + if (buffer) + { + using var bufferedStream = new BufferedReadStream(stream); + return action(bufferedStream); + } + return action(stream); } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs index 6f3ea0e14..389a74326 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DoubleBufferedStreams.cs @@ -19,8 +19,16 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg private MemoryStream stream2; private MemoryStream stream3; private MemoryStream stream4; + private MemoryStream stream5; + private MemoryStream stream6; + private MemoryStream stream7; + private MemoryStream stream8; private DoubleBufferedStreamReader reader1; private DoubleBufferedStreamReader reader2; + private BufferedReadStream bufferedStream1; + private BufferedReadStream bufferedStream2; + private BufferedReadStream2 bufferedStream3; + private BufferedReadStream2 bufferedStream4; [GlobalSetup] public void CreateStreams() @@ -29,8 +37,16 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg this.stream2 = new MemoryStream(this.buffer); this.stream3 = new MemoryStream(this.buffer); this.stream4 = new MemoryStream(this.buffer); + this.stream5 = new MemoryStream(this.buffer); + this.stream6 = new MemoryStream(this.buffer); + this.stream7 = new MemoryStream(this.buffer); + this.stream8 = new MemoryStream(this.buffer); this.reader1 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2); this.reader2 = new DoubleBufferedStreamReader(Configuration.Default.MemoryAllocator, this.stream2); + this.bufferedStream1 = new BufferedReadStream(this.stream5); + this.bufferedStream2 = new BufferedReadStream(this.stream6); + this.bufferedStream3 = new BufferedReadStream2(this.stream7); + this.bufferedStream4 = new BufferedReadStream2(this.stream8); } [GlobalCleanup] @@ -40,8 +56,74 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg this.stream2?.Dispose(); this.stream3?.Dispose(); this.stream4?.Dispose(); + this.stream5?.Dispose(); + this.stream6?.Dispose(); this.reader1?.Dispose(); this.reader2?.Dispose(); + this.bufferedStream1?.Dispose(); + this.bufferedStream2?.Dispose(); + this.bufferedStream3?.Dispose(); + this.bufferedStream4?.Dispose(); + } + + [Benchmark] + public int StandardStreamRead() + { + int r = 0; + Stream stream = this.stream1; + byte[] b = this.chunk1; + + for (int i = 0; i < stream.Length / 2; i++) + { + r += stream.Read(b, 0, 2); + } + + return r; + } + + [Benchmark] + public int DoubleBufferedStreamRead() + { + int r = 0; + DoubleBufferedStreamReader reader = this.reader2; + byte[] b = this.chunk2; + + for (int i = 0; i < reader.Length / 2; i++) + { + r += reader.Read(b, 0, 2); + } + + return r; + } + + [Benchmark] + public int BufferedStreamRead() + { + int r = 0; + BufferedReadStream reader = this.bufferedStream2; + byte[] b = this.chunk2; + + for (int i = 0; i < reader.Length / 2; i++) + { + r += reader.Read(b, 0, 2); + } + + return r; + } + + [Benchmark] + public int BufferedStreamWrapRead() + { + int r = 0; + BufferedReadStream2 reader = this.bufferedStream3; + byte[] b = this.chunk2; + + for (int i = 0; i < reader.Length / 2; i++) + { + r += reader.Read(b, 0, 2); + } + + return r; } [Benchmark(Baseline = true)] @@ -59,25 +141,24 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg } [Benchmark] - public int StandardStreamRead() + public int DoubleBufferedStreamReadByte() { int r = 0; - Stream stream = this.stream1; - byte[] b = this.chunk1; + DoubleBufferedStreamReader reader = this.reader1; - for (int i = 0; i < stream.Length / 2; i++) + for (int i = 0; i < reader.Length; i++) { - r += stream.Read(b, 0, 2); + r += reader.ReadByte(); } return r; } [Benchmark] - public int DoubleBufferedStreamReadByte() + public int BufferedStreamReadByte() { int r = 0; - DoubleBufferedStreamReader reader = this.reader1; + BufferedReadStream reader = this.bufferedStream2; for (int i = 0; i < reader.Length; i++) { @@ -88,22 +169,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg } [Benchmark] - public int DoubleBufferedStreamRead() + public int BufferedStreamWrapReadByte() { int r = 0; - DoubleBufferedStreamReader reader = this.reader2; - byte[] b = this.chunk2; + BufferedReadStream2 reader = this.bufferedStream4; - for (int i = 0; i < reader.Length / 2; i++) + for (int i = 0; i < reader.Length; i++) { - r += reader.Read(b, 0, 2); + r += reader.ReadByte(); } return r; } [Benchmark] - public int SimpleReadByte() + public int ArrayReadByte() { byte[] b = this.buffer; int r = 0; diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs new file mode 100644 index 000000000..992e2536d --- /dev/null +++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.IO; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.IO +{ + public class BufferedReadStreamTests + { + [Fact] + public void BufferedStreamCanReadSingleByteFromOrigin() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(stream)) + { + Assert.Equal(expected[0], reader.ReadByte()); + + // We've read a whole chunk but increment by 1 in our reader. + Assert.Equal(BufferedReadStream.BufferLength, stream.Position); + Assert.Equal(1, reader.Position); + } + + // Position of the stream should be reset on disposal. + Assert.Equal(1, stream.Position); + } + } + + [Fact] + public void BufferedStreamCanReadSingleByteFromOffset() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + const int offset = 5; + using (var reader = new BufferedReadStream(stream)) + { + reader.Position = offset; + + Assert.Equal(expected[offset], reader.ReadByte()); + + // We've read a whole chunk but increment by 1 in our reader. + Assert.Equal(BufferedReadStream.BufferLength + offset, stream.Position); + Assert.Equal(offset + 1, reader.Position); + } + + Assert.Equal(offset + 1, stream.Position); + } + } + + [Fact] + public void BufferedStreamCanReadSubsequentSingleByteCorrectly() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + int i; + using (var reader = new BufferedReadStream(stream)) + { + for (i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], reader.ReadByte()); + Assert.Equal(i + 1, reader.Position); + + if (i < BufferedReadStream.BufferLength) + { + Assert.Equal(stream.Position, BufferedReadStream.BufferLength); + } + else if (i >= BufferedReadStream.BufferLength && i < BufferedReadStream.BufferLength * 2) + { + // We should have advanced to the second chunk now. + Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); + } + else + { + // We should have advanced to the third chunk now. + Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); + } + } + } + + Assert.Equal(i, stream.Position); + } + } + + [Fact] + public void BufferedStreamCanReadMultipleBytesFromOrigin() + { + using (MemoryStream stream = this.CreateTestStream()) + { + var buffer = new byte[2]; + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(stream)) + { + Assert.Equal(2, reader.Read(buffer, 0, 2)); + Assert.Equal(expected[0], buffer[0]); + Assert.Equal(expected[1], buffer[1]); + + // We've read a whole chunk but increment by the buffer length in our reader. + Assert.Equal(stream.Position, BufferedReadStream.BufferLength); + Assert.Equal(buffer.Length, reader.Position); + } + } + } + + [Fact] + public void BufferedStreamCanReadSubsequentMultipleByteCorrectly() + { + using (MemoryStream stream = this.CreateTestStream()) + { + var buffer = new byte[2]; + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(stream)) + { + for (int i = 0, o = 0; i < expected.Length / 2; i++, o += 2) + { + Assert.Equal(2, reader.Read(buffer, 0, 2)); + Assert.Equal(expected[o], buffer[0]); + Assert.Equal(expected[o + 1], buffer[1]); + Assert.Equal(o + 2, reader.Position); + + int offset = i * 2; + if (offset < BufferedReadStream.BufferLength) + { + Assert.Equal(stream.Position, BufferedReadStream.BufferLength); + } + else if (offset >= BufferedReadStream.BufferLength && offset < BufferedReadStream.BufferLength * 2) + { + // We should have advanced to the second chunk now. + Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 2); + } + else + { + // We should have advanced to the third chunk now. + Assert.Equal(stream.Position, BufferedReadStream.BufferLength * 3); + } + } + } + } + } + + [Fact] + public void BufferedStreamCanSkip() + { + using (MemoryStream stream = this.CreateTestStream()) + { + byte[] expected = stream.ToArray(); + using (var reader = new BufferedReadStream(stream)) + { + int skip = 50; + int plusOne = 1; + int skip2 = BufferedReadStream.BufferLength; + + // Skip + reader.Skip(skip); + Assert.Equal(skip, reader.Position); + Assert.Equal(stream.Position, reader.Position); + + // Read + Assert.Equal(expected[skip], reader.ReadByte()); + + // Skip Again + reader.Skip(skip2); + + // First Skip + First Read + Second Skip + int position = skip + plusOne + skip2; + + Assert.Equal(position, reader.Position); + Assert.Equal(stream.Position, reader.Position); + Assert.Equal(expected[position], reader.ReadByte()); + } + } + } + + [Fact] + public void BufferedStreamReadsSmallStream() + { + // Create a stream smaller than the default buffer length + using (MemoryStream stream = this.CreateTestStream(BufferedReadStream.BufferLength / 4)) + { + byte[] expected = stream.ToArray(); + const int offset = 5; + using (var reader = new BufferedReadStream(stream)) + { + reader.Position = offset; + + Assert.Equal(expected[offset], reader.ReadByte()); + + // We've read a whole length of the stream but increment by 1 in our reader. + Assert.Equal(BufferedReadStream.BufferLength / 4, stream.Position); + Assert.Equal(offset + 1, reader.Position); + } + + Assert.Equal(offset + 1, stream.Position); + } + } + + private MemoryStream CreateTestStream(int length = BufferedReadStream.BufferLength * 3) + { + var buffer = new byte[length]; + var random = new Random(); + random.NextBytes(buffer); + + return new MemoryStream(buffer); + } + } +}