// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Buffers; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Benchmarks.IO; /// /// A readonly stream wrapper 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 BufferedReadStreamWrapper : 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; // Index within our buffer, not reader position. private int readBufferIndex; // Matches what the stream position would be without buffering private long readerPosition; private bool isDisposed; /// /// Initializes a new instance of the class. /// /// The input stream. public BufferedReadStreamWrapper(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 = 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 { get; } /// /// Gets or sets the current position within the stream. /// public long Position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.readerPosition; [MethodImpl(MethodImplOptions.NoInlining)] set { // Only reset readBufferIndex if we are out of bounds of our working buffer // otherwise we should simply move the value by the diff. if (this.IsInReadBuffer(value, out long index)) { this.readBufferIndex = (int)index; this.readerPosition = value; } else { // Base stream seek will throw for us if invalid. this.stream.Seek(value, SeekOrigin.Begin); this.readerPosition = value; this.readBufferIndex = BufferLength; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ReadByte() { if (this.readerPosition >= this.Length) { 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 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 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 long Seek(long offset, SeekOrigin origin) { switch (origin) { case SeekOrigin.Begin: this.Position = offset; break; case SeekOrigin.Current: this.Position += offset; break; case SeekOrigin.End: this.Position = this.Length - offset; break; } return this.readerPosition; } /// public void Dispose() { if (!this.isDisposed) { this.isDisposed = true; this.readBufferHandle.Dispose(); ArrayPool.Shared.Return(this.readBuffer); this.Flush(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsInReadBuffer(long newPosition, out long index) { index = newPosition - this.readerPosition + 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.AggressiveInlining)] 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) { long n = this.Length - this.readerPosition; if (n > count) { return count; } if (n < 0) { return 0; } return (int)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); } } }