//******************************************************************************************************
// BlockAllocatedMemoryStream.cs - Gbtc
//
// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 06/14/2013 - J. Ritchie Carroll
// Adapted from the "MemoryTributary" class written by Sebastian Friston:
// Source Code: http://memorytributary.codeplex.com/
// Article: http://www.codeproject.com/Articles/348590/A-replacement-for-MemoryStream
// 11/21/2016 - Steven E. Chisholm
// A complete refresh of BlockAllocatedMemoryStream and how it works.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.IO;
using GSF.Collections;
// ReSharper disable VirtualMemberCallInConstructor
namespace GSF.IO
{
///
/// Defines a stream whose backing store is memory. Externally this class operates similar to a ,
/// internally it uses dynamically allocated buffer blocks instead of one large contiguous array of data.
///
///
///
/// The has two primary benefits over a normal , first, the
/// allocation of a large contiguous array of data in can fail when the requested amount of contiguous
/// memory is unavailable - the prevents this; second, a will
/// constantly reallocate the buffer size as the stream grows and shrinks and then copy all the data from the old buffer to the
/// new - the maintains its blocks over its life cycle, unless manually cleared, thus
/// eliminating unnecessary allocations and garbage collections when growing and reusing a stream.
///
///
/// Important: Unlike , the will not use a user provided buffer
/// as its backing buffer. Any user provided buffers used to instantiate the class will be copied into internally managed reusable
/// memory buffers. Subsequently, the does not support the notion of a non-expandable
/// stream. If you are using a with your own buffer, the will
/// not provide any immediate benefit.
///
///
/// Note that the will maintain all allocated blocks for stream use until the
/// method is called or the class is disposed.
///
///
/// No members in the are guaranteed to be thread safe. Make sure any calls are
/// synchronized when simultaneously accessed from different threads.
///
///
public class BlockAllocatedMemoryStream : Stream
{
// Note: Since byte blocks are pooled, they will not be
// initialized unless a Read/Write operation occurs
// when m_position > m_length
#region [ Members ]
// Constants
private const int BlockSize = 8 * 1024;
private const int ShiftBits = 3 + 10;
private const int BlockMask = BlockSize - 1;
// Fields
private List m_blocks;
private long m_length;
private long m_position;
private long m_capacity;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of .
///
public BlockAllocatedMemoryStream()
{
m_blocks = new List();
}
///
/// Initializes a new instance of from specified .
///
/// Initial buffer to copy into stream.
/// is null.
///
/// Unlike , the will not use the provided
/// as its backing buffer. The buffer will be copied into internally managed reusable
/// memory buffers. Subsequently, the notion of a non-expandable stream is not supported.
///
public BlockAllocatedMemoryStream(byte[] buffer)
: this(buffer, 0, (object)buffer == null ? 0 : buffer.Length)
{
}
///
/// Initializes a new instance of from specified region of .
///
/// Initial buffer to copy into stream.
/// 0-based start index into the .
/// Valid number of bytes within from .
/// is null.
///
/// or is less than 0 -or-
/// and will exceed length.
///
///
/// Unlike , the will not use the provided
/// as its backing buffer. The buffer will be copied into internally managed reusable
/// memory buffers. Subsequently, the notion of a non-expandable stream is not supported.
///
public BlockAllocatedMemoryStream(byte[] buffer, int startIndex, int length)
: this()
{
buffer.ValidateParameters(startIndex, length);
Write(buffer, startIndex, length);
}
///
/// Initializes a new instance of for specified .
///
/// Initial length of the stream.
public BlockAllocatedMemoryStream(int capacity)
: this()
{
SetLength(capacity);
}
#endregion
#region [ Properties ]
///
/// Gets a value that indicates whether the object supports reading.
///
///
/// This is always true.
///
public override bool CanRead => true;
///
/// Gets a value that indicates whether the object supports seeking.
///
///
/// This is always true.
///
public override bool CanSeek => true;
///
/// Gets a value that indicates whether the object supports writing.
///
///
/// This is always true.
///
public override bool CanWrite => true;
///
/// Gets current stream length for this instance.
///
/// The stream is closed.
public override long Length
{
get
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
return m_length;
}
}
///
/// Gets current stream position for this instance.
///
/// Seeking was attempted before the beginning of the stream.
/// The stream is closed.
public override long Position
{
get
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
return m_position;
}
set
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
if (value < 0L)
throw new IOException("Seek was attempted before the beginning of the stream.");
m_position = value;
}
}
#endregion
#region [ Methods ]
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; otherwise, false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
// Make sure buffer blocks get returned to the pool
if (disposing)
Clear();
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
}
///
/// Clears the entire contents and releases any allocated memory blocks.
///
public void Clear()
{
m_position = 0;
m_length = 0;
m_capacity = 0;
// In the event that an exception occurs, we don't want to have released blocks that are still in this memory stream.
List blocks = m_blocks;
m_blocks = new List();
foreach (var block in blocks)
MemoryBlockPool.Enqueue(block);
}
///
/// Sets the within the current stream to the specified value relative the .
///
///
/// The new position within the stream, calculated by combining the initial reference point and the offset.
///
/// The new position within the stream. This is relative to the parameter, and can be positive or negative.
/// A value of type , which acts as the seek reference point.
/// Seeking was attempted before the beginning of the stream.
/// The stream is closed.
public override long Seek(long offset, SeekOrigin origin)
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
switch (origin)
{
case SeekOrigin.Begin:
if (offset < 0L)
throw new IOException("Seek was attempted before the beginning of the stream.");
m_position = offset;
break;
case SeekOrigin.Current:
if (m_position + offset < 0L)
throw new IOException("Seek was attempted before the beginning of the stream.");
m_position += offset;
break;
case SeekOrigin.End:
if (m_length + offset < 0L)
throw new IOException("Seek was attempted before the beginning of the stream.");
m_position = m_length + offset;
break;
}
// Note: the length is not adjusted after this seek to reflect what MemoryStream.Seek does
return m_position;
}
///
/// Sets the length of the current stream to the specified value.
///
/// The value at which to set the length.
///
/// If this length is larger than the previous length, the data is initialized to 0's between the previous length and the current length.
///
public override void SetLength(long value)
{
if (value > m_capacity)
EnsureCapacity(value);
if (m_length < value)
InitializeToPosition(value);
m_length = value;
if (m_position > m_length)
m_position = m_length;
}
///
/// Reads a block of bytes from the current stream and writes the data to .
///
/// When this method returns, contains the specified byte array with the values between and ( + - 1) replaced by the characters read from the current stream.
/// The byte offset in at which to begin reading.
/// The maximum number of bytes to read.
///
/// The total number of bytes written into the buffer. This can be less than the number of bytes requested if that number of bytes are not currently available, or zero if the end of the stream is reached before any bytes are read.
///
/// is null.
///
/// or is less than 0 -or-
/// and will exceed length.
///
/// The stream is closed.
public override int Read(byte[] buffer, int startIndex, int length)
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
buffer.ValidateParameters(startIndex, length);
// Do not read beyond the end of the stream
long remainingBytes = m_length - m_position;
if (remainingBytes <= 0)
return 0;
if (length > remainingBytes)
length = (int)remainingBytes;
int bytesRead = length;
// Must read 1 block at a time
do
{
int blockOffset = (int)(m_position & BlockMask);
int bytesToRead = Math.Min(length, BlockSize - blockOffset);
Buffer.BlockCopy(m_blocks[(int)(m_position >> ShiftBits)], blockOffset, buffer, startIndex, bytesToRead);
length -= bytesToRead;
startIndex += bytesToRead;
m_position += bytesToRead;
}
while (length > 0);
return bytesRead;
}
///
/// Reads a byte from the current stream.
///
///
/// The current byte cast to an , or -1 if the end of the stream has been reached.
///
/// The stream is closed.
public override int ReadByte()
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
if (m_position >= m_length)
return -1;
byte value = m_blocks[(int)(m_position >> ShiftBits)][(int)(m_position & BlockMask)];
m_position++;
return value;
}
///
/// Writes a block of bytes to the current stream using data read from .
///
/// The buffer to write data from.
/// The byte offset in at which to begin writing from.
/// The maximum number of bytes to write.
/// is null.
///
/// or is less than 0 -or-
/// and will exceed length.
///
/// The stream is closed.
public override void Write(byte[] buffer, int startIndex, int length)
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
buffer.ValidateParameters(startIndex, length);
if (m_position + length > m_capacity)
EnsureCapacity(m_position + length);
if (m_position > m_length)
InitializeToPosition(m_position);
if (m_length < m_position + length)
m_length = m_position + length;
if (length == 0)
return;
do
{
int blockOffset = (int)(m_position & BlockMask);
int bytesToWrite = Math.Min(length, BlockSize - blockOffset);
Buffer.BlockCopy(buffer, startIndex, m_blocks[(int)(m_position >> ShiftBits)], blockOffset, bytesToWrite);
length -= bytesToWrite;
startIndex += bytesToWrite;
m_position += bytesToWrite;
}
while (length > 0);
}
///
/// Writes a byte to the current stream at the current position.
///
/// The byte to write.
/// The stream is closed.
public override void WriteByte(byte value)
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
if (m_position + 1 > m_capacity)
EnsureCapacity(m_position + 1);
if (m_position > m_length)
InitializeToPosition(m_position);
if (m_length < m_position + 1)
m_length = m_position + 1;
m_blocks[(int)(m_position >> ShiftBits)][m_position & BlockMask] = value;
m_position++;
}
///
/// Writes the stream contents to a byte array, regardless of the property.
///
/// A [] containing the current data in the stream
///
/// This may fail if there is not enough contiguous memory available to hold current size of stream.
/// When possible use methods which operate on streams directly instead.
///
/// Cannot create a byte array with more than 2,147,483,591 elements.
/// The stream is closed.
public byte[] ToArray()
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
if (m_length > 0x7FFFFFC7L)
throw new InvalidOperationException("Cannot create a byte array of size " + m_length);
byte[] destination = new byte[m_length];
long originalPosition = m_position;
m_position = 0;
Read(destination, 0, (int)m_length);
m_position = originalPosition;
return destination;
}
///
/// Reads specified number of bytes from source stream into this
/// starting at the current position.
///
/// The stream containing the data to copy
/// The number of bytes to copy
/// The stream is closed.
public void ReadFrom(Stream source, long length)
{
// Note: A faster way would be to write directly to the BlockAllocatedMemoryStream
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
byte[] buffer = MemoryBlockPool.Dequeue();
do
{
int bytesRead = source.Read(buffer, 0, (int)Math.Min(BlockSize, length));
if (bytesRead == 0)
throw new EndOfStreamException();
length -= bytesRead;
Write(buffer, 0, bytesRead);
}
while (length > 0);
MemoryBlockPool.Enqueue(buffer);
}
///
/// Writes the entire stream into destination, regardless of , which remains unchanged.
///
/// The stream onto which to write the current contents.
/// The stream is closed.
public void WriteTo(Stream destination)
{
if (m_disposed)
throw new ObjectDisposedException("BlockAllocatedMemoryStream", "The stream is closed.");
long originalPosition = m_position;
m_position = 0;
CopyTo(destination);
m_position = originalPosition;
}
///
/// Overrides the method so that no action is performed.
///
///
///
/// This method overrides the method.
///
///
/// Because any data written to a object is
/// written into RAM, this method is superfluous.
///
///
public override void Flush()
{
// Nothing to flush...
}
///
/// Makes sure desired can be accommodated by future data accesses.
///
/// Minimum desired stream capacity.
private void EnsureCapacity(long length)
{
while (m_capacity < length)
{
m_blocks.Add(MemoryBlockPool.Dequeue());
m_capacity += BlockSize;
}
}
///
/// Initializes all of the bytes to zero.
///
private void InitializeToPosition(long position)
{
long bytesToClear = position - m_length;
while (bytesToClear > 0)
{
int bytesToClearInBlock = (int)Math.Min(bytesToClear, BlockSize - (m_length & BlockMask));
Array.Clear(m_blocks[(int)(m_length >> ShiftBits)], (int)(m_length & BlockMask), bytesToClearInBlock);
m_length += bytesToClearInBlock;
bytesToClear = position - m_length;
}
}
#endregion
// Allow up to 100 items of 8KB items to remain on the buffer pool. This might need to be increased if the buffer pool becomes more
// extensively used. Allocation Statistics will be logged in the Logger.
private static readonly DynamicObjectPool MemoryBlockPool = new DynamicObjectPool(() => new byte[BlockSize], 100);
}
}