Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api Proposal: Add reader for ReadOnlySequence<T> #27522

Closed
JeremyKuhne opened this issue Oct 2, 2018 · 11 comments
Closed

Api Proposal: Add reader for ReadOnlySequence<T> #27522

JeremyKuhne opened this issue Oct 2, 2018 · 11 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Buffers
Milestone

Comments

@JeremyKuhne
Copy link
Member

Rationale and usage

Reading data out of a ReadOnlySequence<T> (such as those returned via System.IO.Pipelines.ReadResult) is difficult. Writing performant, correct logic that handles data spanning multiple discontinuous segments is hard.

SequenceReader<T> wraps a ReadOnlySequence<T> and provides methods for reading binary and text data with a focus on performance and minimal or zero heap allocations.

This proposal is based on code developed in CoreFXlabs

Take the sample code from the blog post introducing System.IO.Pipelines:

async Task ReadPipeAsync(PipeReader reader)
{
    while (true)
    {
        ReadResult result = await reader.ReadAsync();

        ReadOnlySequence<byte> buffer = result.Buffer;
        SequencePosition? position = null;

        do 
        {
            // Look for a EOL in the buffer
            position = buffer.PositionOf((byte)'\n');

            if (position != null)
            {
                // Process the line
                ProcessLine(buffer.Slice(0, position.Value));
                
                // Skip the line + the \n character (basically position)
                buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            }
        }
        while (position != null);

        // Tell the PipeReader how much of the buffer we have consumed
        reader.AdvanceTo(buffer.Start, buffer.End);

        // Stop reading if there's no more data coming
        if (result.IsCompleted)
        {
            break;
        }
    }

    // Mark the PipeReader as complete
    reader.Complete();
}

Dealing with slicing and SequencePosition is awkward. With SequenceReader<byte> the sample would be written like this:

async Task ReadPipeAsync(PipeReader reader)
{
    while (true)
    {
        ReadResult result = await reader.ReadAsync();

        // Tell the PipeReader how much of the buffer we have consumed
        reader.AdvanceTo(ProcessLines(result.Buffer));

        // Stop reading if there's no more data coming
        if (result.IsCompleted)
        {
            break;
        }
    }

    // Mark the PipeReader as complete
    reader.Complete();
}

private static SequencePosition ProcessLines(ReadOnlySequence<byte> sequence)
{
    SequenceReader<byte> reader = new SequenceReader<byte>(sequence);

    while (reader.TryReadTo(out ReadOnlySequence<byte> line, (byte)'\n'))
    {
        // Process the line
        ProcessLine(line);
    }

    return reader.Position;
}

C# static local methods will allow making this particular example even cleaner as you'll be able to pull the reader (which is ref) into a local method when using async.

Here is another example of how the reader would be used to split a UTF-8 sequence into CRLF delimited lines, rejecting any loose or reversed line end characters.

public static void ReadHeaderLines(ReadOnlySequence<byte> sequence)
{
    BufferReader<byte> reader = new BufferReader<byte>(sequence);
    ReadOnlySpan<byte> crlf = s_crlf;

    while (!reader.End)
    {
        if (!reader.TryReadToAny(out ReadOnlySpan<byte> headerLine, crlf, advancePastDelimiter: false))
        {
            // Couldn't find another delimiter
        }

        if (!reader.IsNext(crlf, advancePast: true))
        {
            // Not a good CR/LF pair
        }

        // headerLine is a known good line
    }
}

To pause / recontinue reading the input data should be sliced. For example:

public static ReadOnlySequence<byte> DoStuff(ReadOnlySequence<byte> data)
{
    SequenceReader<byte> reader = new SequenceReader<byte>(data);

    /* Do whatever */

    // Return the remaining data
    return data.Slice(reader.Position);
}

Key design considerations

SequenceReader<T> is designed with the following considerations.

  1. It must be fast
    a. The current ReadOnlySpan<T> needs to be cached to achieve this (which necessitates SequenceReader<T> being ref)
    b. Reading data that is constrained to a single segment should be as fast as possible.
  2. There must be zero allocation alternatives for all functionality
    a. Using methods that don't return ReadOnlySpan<T> never heap allocate.
    b. ReadOnlySpan<T>
  3. It should support UTF-8 and UTF-16 data
    a. Extension methods presume SequenceReader<byte> has UTF-8 data, SequenceReader<char> has UTF-16 data
    b. UTF-16 functionality currently blocked on Utf16Parser shipping.
  4. It is constrained to unmanaged and IEquatable<T>
    a. unmanaged is necessary to do work on the stack (performance)
    b. IEquatable<T> is necessary to parse

API surface area

namespace System.Buffers
{
    public ref struct SequenceReader<T> where T : unmanaged, IEquatable<T>
    {
        // Used to provide custom allocation functionality when concatenating is necessary
        public delegate Span<T> Allocator(int length);

        public SequenceReader(in ReadOnlySequence<T> buffer, Allocator allocator = null);

        // State
        public bool End { get; }

        // This is awkward- we can know that we are in the last segment quickly
        // but we can't express that there are no more spans without costly checks
        // as if we have more segments they may potentially be empty.
        // 
        // One other option is to expose the next segment (SequencePosition NextSegment { get; }).
        // It is a little awkward as you have to call a method hidden to Intellisense to know that you're
        // at the end- it would be reader.NextSegment.GetObject() == null.
        public bool AtLastSegment { get; }

        public void Advance(long count);
        public void Rewind(long count);

        public long Consumed { get; }                // The total advanced
        public ReadOnlySpan<T> CurrentSpan { get; }
        public int CurrentSpanIndex { get; }
        public ReadOnlySpan<T> UnreadSpan { get; }

        public SequencePosition Position { get; }
        public ReadOnlySequence<T> Sequence { get; } // The original ReadOnlySequence<T>

        // Peek does not advance
        public bool TryPeek(out T value);
        public readonly ReadOnlySpan<T> Peek(Span<T> copyBuffer);

        // Read will advance if successful
        public bool TryRead(out T value);

        public bool IsNext(T value, bool advancePast = false);
        public readonly bool IsNext(ReadOnlySpan<T> next, bool advancePast = false);
        public bool AreAnyNext(T value0, T value1, bool advancePast = false);
        public bool AreAnyNext(T value0, T value1, T value2, bool advancePast = false);
        public bool AreAnyNext(T value0, T value1, T value2, T value3, bool advancePast = false);
        public readonly bool AreAnyNext(ReadOnlySpan<T> values, bool advancePast = false);

        public long AdvancePast(T value);
        public long AdvancePastAny(T value0, T value1);
        public long AdvancePastAny(T value0, T value1, T value2);
        public long AdvancePastAny(T value0, T value1, T value2, T value3);
        public long AdvancePastAny(ReadOnlySpan<T> values);
        public bool TryAdvanceTo(T delimiter, bool advancePastDelimiter = true);
        public bool TryAdvanceToAny(ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);

        public bool TryReadTo(out ReadOnlySpan<T> span, T delimiter, bool advancePastDelimiter = true);
        public bool TryReadTo(out ReadOnlySpan<T> span, T delimiter, T delimiterEscape, bool advancePastDelimiter = true);
        public bool TryReadTo(out ReadOnlySpan<T> span, ReadOnlySpan<T> delimiter, bool advancePastDelimiter = true);
        public bool TryReadToAny(out ReadOnlySpan<T> span, ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);

        public bool TryReadTo(out ReadOnlySequence<T> sequence, T delimiter, bool advancePastDelimiter = true);
        public bool TryReadTo(out ReadOnlySequence<T> sequence, T delimiter, T delimiterEscape, bool advancePastDelimiter = true);
        public bool TryReadTo(out ReadOnlySequence<T> sequence, ReadOnlySpan<T> delimiter, bool advancePastDelimiter = true);
        public bool TryReadToAny(out ReadOnlySequence<T> sequence, ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
    }

    public static class SequenceReaderExtensions
    {
        // Utf8 parsing extensions
        public static bool TryParse(this ref SequenceReader<byte> reader, out bool value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out byte value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out DateTime value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out DateTimeOffset value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out decimal value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out double value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out Guid value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out short value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out int value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out long value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out sbyte value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out float value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out TimeSpan value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out ushort value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out uint value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<byte> reader, out ulong value, char standardFormat = '\0');

        // Binary reading extensions
        public static bool TryRead<T>(this ref SequenceReader<byte> reader, out T value) where T : struct, ValueType reqmod UnmanagedType;
        public static bool TryReadInt16BigEndian(this ref SequenceReader<byte> reader, out short value);
        public static bool TryReadInt16LittleEndian(this ref SequenceReader<byte> reader, out short value);
        public static bool TryReadInt32BigEndian(this ref SequenceReader<byte> reader, out int value);
        public static bool TryReadInt32LittleEndian(this ref SequenceReader<byte> reader, out int value);
        public static bool TryReadInt64BigEndian(this ref SequenceReader<byte> reader, out long value);
        public static bool TryReadInt64LittleEndian(this ref SequenceReader<byte> reader, out long value);

        // Utf16 parsing extensions (Blocked on shipping Utf16Parser)
        public static bool TryParse(this ref SequenceReader<char> reader, out bool value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out byte value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out DateTime value,, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out DateTimeOffset value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out decimal value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out double value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out Guid value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out short value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out int value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out long value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out sbyte value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out float value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out TimeSpan value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out ushort value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out uint value, char standardFormat = '\0');
        public static bool TryParse(this ref SequenceReader<char> reader, out ulong value, char standardFormat = '\0');
    }

    public static class BuffersExtensions
    {
        // If this is a single segment, just returns the span. Otherwise copies to the copyBuffer.
        // Throws ArgumentOutOfRange if copyBuffer isn't large enough.
        public static ReadOnlySpan<T> AsSpan<T>(in this ReadOnlySequence<T> source, Span<T> copyBuffer);
    }

    // Should this be an extension method?
    public readonly partial struct ReadOnlySequence<T>
    {
        // In the reader we frequently have to get an updated position that we *know* is within
        // the current segment. It is significantly faster to cut out the checks that GetPosition() makes.
        // The implementation is:
        //      new SequencePosition(position.GetObject(), offset + (GetIndex(in position)));
        public SequencePosition UnsafeGetPositionWithinSegment(in SequencePosition position, long offset);
    }
}

API details

General

This API is based off of prototype code in CoreFXLab which has been tested against a prototype JSON reader.

advancePast is provided as an optional parameter when searching as it can reduce subsequent calls to Advance. This has a measurable impact on performance.

Methods that return ReadOnlySequence<T> never allocate.

Methods that return ReadOnlySpan<T> will allocate when needed to aggregate.

Any methods are far faster when checking individual T's up to 4 arguments.

When at the end, End will be true and the Position will be at the end of the last segment.

Peeking ahead

In order to be performant, Peek will return slices of existing data where possible.

// Only copies to copyBuffer if needed, otherwise just slices
public ReadOnlySpan<T> Peek(Span<T> copyBuffer);

Parsing

The Utf8Parser.TryParse methods are open ended in what they'll read in. SequenceReader will be capped at 128 items (bytes/chars). If we can't parse successfully with less it will be considered a failed parse. (See issues below)

Futures

BufferWriter<T> is still in early development, will work with existing IBufferWriter<T>.

String convenience methods may be useful. (ReadLine, etc.)

Should look into providing adapters for reading Stream (need to think how one creates as UTF-16).

We should consider supporting Augmented Backus-Naur rule sets for parsing as they figure into many specifications. Something like:

[Flags]
public enum BackusNaurRules
{
    Alpha,
    Digit,
    HexDigit,
    DoubleQuote,
    Space,
    HorizontalTab,
    WhiteSpace,
    LinearWhiteSpace,
    VisibleChar,
    Char,
    Octet,
    Control,
    CarriageReturn,
    LineFeed,
    NewLine,
    Bit
}

public bool TryRead(out Span<T> value, BackusNaurRules matches);

Issue summary

Allocating spans cross segment
Currently we don't provide a way to specify how we allocate when aggregating a span across a segment boundary. It is presumed that such allocating won't be frequent, and in user cases where it would be the ReadOnlySequence overloads allow custom aggregation. We could add additional overrloads where you provide a destination buffer, but that introduces additional complexity in needing to return more state (i.e. was there no match or did we run out of buffer space).

To allow another way to control allocations in the fast path (Span returning methods) we'll allow passing in a custom delegate to allocate. By default we'll just allocate new arrays if this isn't specified.

System.Buffers.BuffersExtensions.CopyTo allows copying to a span, but in many cases we'll only need to slice. The proposal above adds a new extension method to address this.

Missing Utf16Parser
We didn't ship Utf16Parser when we added Utf8Parser. Utf8Parser works fundamentally different than historical TryParse functionality as it reads until it has a valid data set and stops at further invalid data (with a number of caveats). Historical TryParse functionality requires all passed in data be valid.

Parsing must be bounded
Utf8Parser is effectively unbounded for reading in data. Numbers can have any number of insignificant zeroes in them, for example. It also does not return rich error state when it can't parse. This means that we can't easily know when we have failed due to needing more data. Trying to parse all available data is also a potential DOS as we would need to aggregate data up to int.MaxValue to make a call that we would still have to shrug off as ReadOnlySequence<T> is constrained by long, not int.

SequenceReader<T> parsing methods deal with this by capping parsing at 128 elements (for all currently implemented numeric types). If we can't get a successful parse from Utf8Parser with less than 128 elements, SequenceReader<T> will consider the parse attempt to be unsuccessful.

SequenceReader can't be used in async methods
As it is a ref struct. Other features, such as C# static local methods will mitigate this restriction.

Can't stackalloc without unsafe
SequenceReader can't recieve "safe" stackalloc spans Span<byte> span = stackalloc byte[128]. Peek() requires you to use unsafe to use stackalloc'ed spans. Peek would be addressed by dotnet/csharplang#1710, which is currently on track for 3.0 and is reflected above (the readonly modifier). Other potential methods that would take an output span buffer that modify struct state (i.e. that would advance the reader) would require some way to annotate that they will not capture the output span. Here is an example usage that requires unsafe currently:

byte* buffer = stackalloc byte[sizeof(T)];
Span<byte> tempSpan = new Span<byte>(buffer, sizeof(T));

if (reader.Peek(tempSpan).Length < sizeof(T))
{
    value = default;
    return false;
}

value = MemoryMarshal.Read<T>(tempSpan);
reader.Advance(sizeof(T));
return true;

SequenceReader is large
SequenceReader contains over 90 bytes of fields (with alignment and two SequencePositions, a ReadOnlySequence, a ReadOnlySpan, etc.). Passing by reference is necessary to get the best performance. What little can be done to reduce the size (maybe 4 bytes) introduces significant complexity (borrowing bits for example). Cutting the fields down further introduces significant drops in performance (e.g. needing to reseek with every call).

SpanReader
Spans can't be wrapped in ReadOnlySequence, which makes SequenceReader unusable with ReadOnlySpan (Memory<T> can be wrapped, however). While it is technically possible to make SequenceReader two-state and have it also take a ReadOnlySpan, it has the following drawbacks:

  1. Some methods/properties on SequenceReader can't be used (such as the ReadOnlySequence returning methods).
  2. Methods that do work are often significantly slower than an equivalent would be for a Span only reader (10-80% in initial measurements)
  3. Construcing a SequenceReader is significantly more costly than a Span specific reader would be (4x as slow, over 2x as large)
  4. SequenceReader would get larger to hold the Span and the context (another 16 bytes likely).

Presumption is that if you're starting with a Span, you'll often not be doing many reads and/or will want any overhead to be as small as possible. Buffers being passed in native interop calls being one scenario where this would be the case. It is often reading an int or a struct and getting a type and/or length that you'll use to iterate through the rest of the buffer.

Proposal is that from a performance & usability standpoint we should have a matching SpanReader that includes all of the properties/methods that make sense to Span. We can also explore adding a BufferReader that has the intersection of the functionality for those that want to be able to handle any incoming data and are willing to take the extra overhead of an adapter.

@JeremyKuhne JeremyKuhne self-assigned this Oct 2, 2018
@JeremyKuhne
Copy link
Member Author

JeremyKuhne commented Oct 2, 2018

@KrzysztofCwalina
Copy link
Member

We need a reader for both ROS and Span. Why cannot this type have a ctor overload taking a Span?

@JeremyKuhne
Copy link
Member Author

We need a reader for both ROS and Span. Why cannot this type have a ctor overload taking a Span?

Taking ReadOnlyMemory<T> would be straight forward- we can add a convenience constructor for that.

Taking ReadOnlySpan<T> would be a little odd. None of the ReadOnlySequence<T> methods would work, Position and Sequence wouldn't make sense. I wouldn't want to have a two state reader where some of the properties/methods are unusable.

@KrzysztofCwalina
Copy link
Member

Position would work just fine with Span. The sequence property does not seem to be very useful. And there is no reason to use TryRead returning ROS when you read a single span.

@JeremyKuhne
Copy link
Member Author

Position would work just fine with Span.

How do you create a SequencePosition from a Span?

The sequence property does not seem to be very useful.

It is important in saving state of the Reader or resetting to the beginning.

And there is no reason to use TryRead returning ROS when you read a single span.

Unless you take the reader as an argument in a helper method that never allocates or wants to pass ROS to some other method.

I think have a two-state reader would be a problem- significant chunks of this struct would have either no meaning or limited value. The biggest value this struct brings is in handling the multi-segment complexity.

It might be useful to aggregate the various span helpers (Utf8Parser, BinaryPrimitives, etc.) with a concept of a position. What I would probably do to allow for that is something like changing the name of this to SequenceReader and investigate having a dedicated SpanReader that gives you said aggregation convenience.

@KrzysztofCwalina
Copy link
Member

How do you create a SequencePosition from a Span?

new SequencePosition(_currentIndex, null);

It is important in saving state of the Reader or resetting to the beginning.

The caller has the original ROS; they passed it to the ctor most likely. Why do they need the property to get it?

Unless you take the reader as an argument in a helper method that never allocates

Is this a real and important scenario? I think there is value in having smaller number of these reader types. The value might be worth the tradeoff, i.e. limiting some scenarios where it's not clear we need them.

I am not sure if this is doable (the right tradeoff), but I think we should think it's worth spending some more time thinking how we could ship a single reader type. It seems like you are giving up too easily :-)

@JeremyKuhne
Copy link
Member Author

I think there is value in having smaller number of these reader types.

While I agree in principle, there is also value in not providing types that solve multiple things poorly.

It seems like you are giving up too easily

I don't think I am.

I know that handling Span will make this type more complicated and harder to use. I think having large swaths of the struct be non-usable in some cases is alone a non-starter. Take all these together and I have a hard time justifing spending the time:

  1. Must know what backs the reader to know what methods you can use
  2. Writing non-allocating helpers/extensions much more difficult
  3. Would have to pay additional (measurable) overhead to handle span only reader case in all methods
  4. SpanReader value over just using existing extensions and slicing not obvious must have
  5. SpanReader would be much more performant if we actually do want it
  6. Must pass additional context with reader in order to reconstruct
  7. Removing the original ROS bloats wrapping structs (such as the JsonReader)

What I could see doing is providing BufferReader that contains either a SequenceReader or a SpanReader and only exposes the always useful APIs.

@KrzysztofCwalina
Copy link
Member

Would have to pay additional (measurable) overhead to handle span only reader case in all methods

Have you measured it? As we talked, my mental model is that the methods simply read from the unread span (in both modes) and the only difference between the modes (from perf perspective) is when you hit the ned of the current span.

SpanReader value over just using existing extensions and slicing not obvious must have

When we shipped the static methods, people (@jkotas and @stephentoub) felt is critical that we have a reader.

SpanReader would be much more performant if we actually do want it

Is that objection different than point 3 above?

Must pass additional context with reader in order to reconstruct

I am not sure I understand. Does your design support such context or it would have to be added to support spans.

@JeremyKuhne
Copy link
Member Author

Is that objection different than point 3 above?

Yes- we can make the code much faster if we never have to go down the road of looking for additional space. Everything I measured (literally weeks of running perf tests) showed impact from the smallest of changes- even one condition would make a difference. I whipped together the start of SpanReader and got the following on the first thing I checked (these are going through the same 100,000 byte backing array):

                 Method |      Mean |     Error |    StdDev |    Median |
----------------------- |----------:|----------:|----------:|----------:|
              ReadInt32 |  66.34 us | 1.3174 us | 2.0511 us |  64.72 us |
   ReadInt32_SpanReader |  53.96 us | 0.1667 us | 0.1301 us |  53.95 us |

Or, perhaps more interestingly, if I mark the method as AggressiveInlining on both:

                 Method |     Mean |     Error |    StdDev |   Median |
----------------------- |---------:|----------:|----------:|---------:|
              ReadInt32 | 69.13 us | 1.3808 us | 2.0239 us | 70.05 us |
   ReadInt32_SpanReader | 40.65 us | 0.8066 us | 1.2557 us | 39.79 us |

Hitting the no-more-data path is obviously really rare in this test. As such the difference in speed is almost entirely from the simple changes to the fast path. I'll put up a PR tomorrow that you can look at once I add a few more methods and tests, but here are the not-so-different implementation differences (of the fast path only):

        public static unsafe bool TryRead<T>(ref this BufferReader<byte> reader, out T value) where T : unmanaged
        {
            ReadOnlySpan<byte> span = reader.UnreadSpan;
            if (span.Length < sizeof(T))
                return TryReadSlow(ref reader, out value);

            value = MemoryMarshal.Read<T>(span);
            reader.Advance(sizeof(T));
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Advance(int count)
        {
            if (count < 0)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
            }

            Consumed += count;

            if (CurrentSpanIndex < CurrentSpan.Length - count)
            {
                CurrentSpanIndex += count;
            }
            else
            {
                // Current segment doesn't have enough space, scan forward through segments
                AdvanceToNextSpan(count);
            }
        }
        public static unsafe bool TryRead<T>(ref this SpanReader<byte> reader, out T value) where T : unmanaged
        {
            ReadOnlySpan<byte> span = reader.UnreadSpan;
            if (span.Length < sizeof(T))
            {
                value = default;
                return false;
            }

            value = MemoryMarshal.Read<T>(span);
            reader.Advance(sizeof(T));
            return true;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Advance(int count)
        {
            if (count < 0 || count > UnreadSpan.Length)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
            }

            Consumed += count;
            UnreadSpan = UnreadSpan.Slice(count);
        }

Does your design support such context or it would have to be added to support spans.

The current design doesn't need any extra context to save state. If we removed some of the members you'd need to hold onto extra data to save state.

@JeremyKuhne
Copy link
Member Author

Changing the name to SequenceReader<T> for now to reflect that is what it works on and add an issue describing options to work with Span.

@ahsonkhan
Copy link
Member

@JeremyKuhne, given dotnet/corefx#33288, can this issue be closed now or is there still some work left? If so, can we break out what's left into a separate issue?

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Buffers
Projects
None yet
Development

No branches or pull requests

4 participants