// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
/* Original license and copyright from file copied from https://github.com/AArnott/Nerdbank.Streams/blob/d656899be26d4d7c72c11c9232b4952c64a89bcb/src/Nerdbank.Streams/Sequence%601.cs
* Copyright (c) Andrew Arnott. All rights reserved.
* Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
*/
using System;
using System.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Nerdbank.Streams
{
///
/// Manages a sequence of elements, readily castable as a .
///
/// The type of element stored by the sequence.
///
/// Instance members are not thread-safe.
///
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
internal class Sequence : IBufferWriter, IDisposable
{
private static readonly int DefaultLengthFromArrayPool = 1 + (4095 / Marshal.SizeOf());
private readonly Stack segmentPool = new Stack();
private readonly MemoryPool memoryPool;
private readonly ArrayPool arrayPool;
private SequenceSegment first;
private SequenceSegment last;
///
/// Initializes a new instance of the class
/// that uses a private for recycling arrays.
///
public Sequence()
: this(ArrayPool.Create())
{
}
///
/// Initializes a new instance of the class.
///
/// The pool to use for recycling backing arrays.
public Sequence(MemoryPool memoryPool)
{
Requires.NotNull(memoryPool, nameof(memoryPool));
this.memoryPool = memoryPool;
}
///
/// Initializes a new instance of the class.
///
/// The pool to use for recycling backing arrays.
public Sequence(ArrayPool arrayPool)
{
Requires.NotNull(arrayPool, nameof(arrayPool));
this.arrayPool = arrayPool;
}
///
/// Gets or sets the minimum length for any array allocated as a segment in the sequence.
/// Any non-positive value allows the pool to determine the length of the array.
///
/// The default value is 0.
///
///
/// Each time or is called,
/// previously allocated memory is used if it is large enough to satisfy the length demand.
/// If new memory must be allocated, the argument to one of these methods typically dictate
/// the length of array to allocate. When the caller uses very small values (just enough for its immediate need)
/// but the high level scenario can predict that a large amount of memory will be ultimately required,
/// it can be advisable to set this property to a value such that just a few larger arrays are allocated
/// instead of many small ones.
///
///
/// The in use may itself have a minimum array length as well,
/// in which case the higher of the two minimums dictate the minimum array size that will be allocated.
///
///
public int MinimumSpanLength { get; set; } = 0;
///
/// Gets this sequence expressed as a .
///
/// A read only sequence representing the data in this object.
public ReadOnlySequence AsReadOnlySequence => this;
///
/// Gets the length of the sequence.
///
public long Length => this.AsReadOnlySequence.Length;
///
/// Gets the value to display in a debugger datatip.
///
private string DebuggerDisplay => $"Length: {this.AsReadOnlySequence.Length}";
///
/// Expresses this sequence as a .
///
/// The sequence to convert.
public static implicit operator ReadOnlySequence(Sequence sequence)
{
return sequence.first != null
? new ReadOnlySequence(sequence.first, sequence.first.Start, sequence.last, sequence.last.End)
: ReadOnlySequence.Empty;
}
///
/// Removes all elements from the sequence from its beginning to the specified position,
/// considering that data to have been fully processed.
///
///
/// The position of the first element that has not yet been processed.
/// This is typically after reading all elements from that instance.
///
public void AdvanceTo(SequencePosition position)
{
var firstSegment = (SequenceSegment)position.GetObject();
int firstIndex = position.GetInteger();
// Before making any mutations, confirm that the block specified belongs to this sequence.
var current = this.first;
while (current != firstSegment && current != null)
{
current = current.Next;
}
Requires.Argument(current != null, nameof(position), "Position does not represent a valid position in this sequence.");
// Also confirm that the position is not a prior position in the block.
Requires.Argument(firstIndex >= current.Start, nameof(position), "Position must not be earlier than current position.");
// Now repeat the loop, performing the mutations.
current = this.first;
while (current != firstSegment)
{
current = this.RecycleAndGetNext(current);
}
firstSegment.AdvanceTo(firstIndex);
if (firstSegment.Length == 0)
{
firstSegment = this.RecycleAndGetNext(firstSegment);
}
this.first = firstSegment;
if (this.first == null)
{
this.last = null;
}
}
///
/// Advances the sequence to include the specified number of elements initialized into memory
/// returned by a prior call to .
///
/// The number of elements written into memory.
public void Advance(int count)
{
SequenceSegment last = this.last;
Verify.Operation(last != null, "Cannot advance before acquiring memory.");
last.Advance(count);
}
///
/// Gets writable memory that can be initialized and added to the sequence via a subsequent call to .
///
/// The size of the memory required, or 0 to just get a convenient (non-empty) buffer.
/// The requested memory.
public Memory GetMemory(int sizeHint) => this.GetSegment(sizeHint).RemainingMemory;
///
/// Gets writable memory that can be initialized and added to the sequence via a subsequent call to .
///
/// The size of the memory required, or 0 to just get a convenient (non-empty) buffer.
/// The requested memory.
public Span GetSpan(int sizeHint) => this.GetSegment(sizeHint).RemainingSpan;
///
/// Clears the entire sequence, recycles associated memory into pools,
/// and resets this instance for reuse.
/// This invalidates any previously produced by this instance.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public void Dispose() => this.Reset();
///
/// Clears the entire sequence and recycles associated memory into pools.
/// This invalidates any previously produced by this instance.
///
public void Reset()
{
var current = this.first;
while (current != null)
{
current = this.RecycleAndGetNext(current);
}
this.first = this.last = null;
}
private SequenceSegment GetSegment(int sizeHint)
{
Requires.Range(sizeHint >= 0, nameof(sizeHint));
int? minBufferSize = null;
if (sizeHint == 0)
{
if (this.last == null || this.last.WritableBytes == 0)
{
// We're going to need more memory. Take whatever size the pool wants to give us.
minBufferSize = -1;
}
}
else
{
sizeHint = Math.Max(this.MinimumSpanLength, sizeHint);
if (this.last == null || this.last.WritableBytes < sizeHint)
{
minBufferSize = sizeHint;
}
}
if (minBufferSize.HasValue)
{
var segment = this.segmentPool.Count > 0 ? this.segmentPool.Pop() : new SequenceSegment();
if (this.arrayPool != null)
{
segment.Assign(this.arrayPool.Rent(minBufferSize.Value == -1 ? DefaultLengthFromArrayPool : minBufferSize.Value));
}
else
{
segment.Assign(this.memoryPool.Rent(minBufferSize.Value));
}
this.Append(segment);
}
return this.last;
}
private void Append(SequenceSegment segment)
{
if (this.last == null)
{
this.first = this.last = segment;
}
else
{
if (this.last.Length > 0)
{
// Add a new block.
this.last.SetNext(segment);
}
else
{
// The last block is completely unused. Replace it instead of appending to it.
var current = this.first;
if (this.first != this.last)
{
while (current.Next != this.last)
{
current = current.Next;
}
}
else
{
this.first = segment;
}
current.SetNext(segment);
this.RecycleAndGetNext(this.last);
}
this.last = segment;
}
}
private SequenceSegment RecycleAndGetNext(SequenceSegment segment)
{
var recycledSegment = segment;
segment = segment.Next;
recycledSegment.ResetMemory(this.arrayPool);
this.segmentPool.Push(recycledSegment);
return segment;
}
private class SequenceSegment : ReadOnlySequenceSegment
{
///
/// A value indicating whether the element may contain references (and thus must be cleared).
///
private static readonly bool MayContainReferences = !typeof(T).GetTypeInfo().IsPrimitive;
///
/// Gets the backing array, when using an instead of a .
///
private T[] array;
///
/// Gets the position within where the data starts.
///
/// This may be nonzero as a result of calling .
internal int Start { get; private set; }
///
/// Gets the position within where the data ends.
///
internal int End { get; private set; }
///
/// Gets the tail of memory that has not yet been committed.
///
internal Memory RemainingMemory => this.AvailableMemory.Slice(this.End);
///
/// Gets the tail of memory that has not yet been committed.
///
internal Span RemainingSpan => this.AvailableMemory.Span.Slice(this.End);
///
/// Gets the tracker for the underlying array for this segment, which can be used to recycle the array when we're disposed of.
/// Will be null if using an array pool, in which case the memory is held by .
///
internal IMemoryOwner MemoryOwner { get; private set; }
///
/// Gets the full memory owned by the .
///
internal Memory AvailableMemory => this.array ?? this.MemoryOwner?.Memory ?? default;
///
/// Gets the number of elements that are committed in this segment.
///
internal int Length => this.End - this.Start;
///
/// Gets the amount of writable bytes in this segment.
/// It is the amount of bytes between and .
///
internal int WritableBytes => this.AvailableMemory.Length - this.End;
///
/// Gets or sets the next segment in the singly linked list of segments.
///
internal new SequenceSegment Next
{
get => (SequenceSegment)base.Next;
set => base.Next = value;
}
///
/// Assigns this (recyclable) segment a new area in memory.
///
/// The memory and a means to recycle it.
internal void Assign(IMemoryOwner memoryOwner)
{
this.MemoryOwner = memoryOwner;
this.Memory = memoryOwner.Memory;
}
///
/// Assigns this (recyclable) segment a new area in memory.
///
/// An array drawn from an .
internal void Assign(T[] array)
{
this.array = array;
this.Memory = array;
}
///
/// Clears all fields in preparation to recycle this instance.
///
internal void ResetMemory(ArrayPool arrayPool)
{
this.ClearReferences(this.Start, this.End);
this.Memory = default;
this.Next = null;
this.RunningIndex = 0;
this.Start = 0;
this.End = 0;
if (this.array != null)
{
arrayPool.Return(this.array);
this.array = null;
}
else
{
this.MemoryOwner?.Dispose();
this.MemoryOwner = null;
}
}
///
/// Adds a new segment after this one.
///
/// The next segment in the linked list.
internal void SetNext(SequenceSegment segment)
{
Debug.Assert(segment != null, "Null not allowed.");
this.Next = segment;
segment.RunningIndex = this.RunningIndex + this.Start + this.Length;
// When setting Memory, we start with index 0 instead of this.Start because
// the first segment has an explicit index set anyway,
// and we don't want to double-count it here.
this.Memory = this.AvailableMemory.Slice(0, this.Start + this.Length);
}
///
/// Commits more elements as written in this segment.
///
/// The number of elements written.
internal void Advance(int count)
{
Requires.Range(count >= 0 && this.End + count <= this.Memory.Length, nameof(count));
this.End += count;
}
///
/// Removes some elements from the start of this segment.
///
/// The number of elements to ignore from the start of the underlying array.
internal void AdvanceTo(int offset)
{
Debug.Assert(offset >= this.Start, "Trying to rewind.");
this.ClearReferences(this.Start, offset - this.Start);
this.Start = offset;
}
private void ClearReferences(int startIndex, int length)
{
// Clear the array to allow the objects to be GC'd.
// Reference types need to be cleared. Value types can be structs with reference type members too, so clear everything.
if (MayContainReferences)
{
this.AvailableMemory.Span.Slice(startIndex, length).Clear();
}
}
}
}
internal static class Requires
{
///
/// Throws an if a condition does not evaluate to true.
///
[DebuggerStepThrough]
public static void Range(bool condition, string parameterName, string message = null)
{
if (!condition)
{
FailRange(parameterName, message);
}
}
///
/// Throws an if a condition does not evaluate to true.
///
/// Nothing. This method always throws.
[DebuggerStepThrough]
public static Exception FailRange(string parameterName, string message = null)
{
if (string.IsNullOrEmpty(message))
{
throw new ArgumentOutOfRangeException(parameterName);
}
else
{
throw new ArgumentOutOfRangeException(parameterName, message);
}
}
///
/// Throws an exception if the specified parameter's value is null.
///
/// The type of the parameter.
/// The value of the argument.
/// The name of the parameter to include in any thrown exception.
/// The value of the parameter.
/// Thrown if is null.
[DebuggerStepThrough]
public static T NotNull(T value, string parameterName)
where T : class // ensures value-types aren't passed to a null checking method
{
if (value == null)
{
throw new ArgumentNullException(parameterName);
}
return value;
}
///
/// Throws an ArgumentException if a condition does not evaluate to true.
///
[DebuggerStepThrough]
public static void Argument(bool condition, string parameterName, string message)
{
if (!condition)
{
throw new ArgumentException(message, parameterName);
}
}
///
/// Throws an ArgumentException if a condition does not evaluate to true.
///
[DebuggerStepThrough]
public static void Argument(bool condition, string parameterName, string message, object arg1)
{
if (!condition)
{
throw new ArgumentException(String.Format(message, arg1), parameterName);
}
}
///
/// Throws an ArgumentException if a condition does not evaluate to true.
///
[DebuggerStepThrough]
public static void Argument(bool condition, string parameterName, string message, object arg1, object arg2)
{
if (!condition)
{
throw new ArgumentException(String.Format(message, arg1, arg2), parameterName);
}
}
///
/// Throws an ArgumentException if a condition does not evaluate to true.
///
[DebuggerStepThrough]
public static void Argument(bool condition, string parameterName, string message, params object[] args)
{
if (!condition)
{
throw new ArgumentException(String.Format(message, args), parameterName);
}
}
}
///
/// Common runtime checks that throw exceptions upon failure.
///
internal static partial class Verify
{
///
/// Throws an if a condition is false.
///
[DebuggerStepThrough]
internal static void Operation(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
}
}