2

I am consuming Json from a TcpClient, and to get a solution with low allocation and good performance I decided to use the new System.IO.Pipelines for handling IO and System.Text.Json for deserialization. The output from the pipe is a ReadOnlySequence<byte>. I am ok when there is only one segment in the ReadOnlySequence, so that I can pass this segment (which is a ReadOnlySpan<byte>) to the deserializer. But what should I do with multiple segments?

What I have so far is the code below. But in some cases, the length of the sequence is too large, so I get a stack overflow in stackalloc. Also, making a copy of the data seems to me as breaking the intention of System.IO.Pipelines. Shouldn't System.Text.Json.JsonSerializer.Deserialize have a ReadOnlySequence overload? Any suggestions for how this should be solved?

private void ProcessLine(ReadOnlySequence<byte> sequence)
{
    if (sequence.IsSingleSegment)
    {
        _result = JsonSerializer.Deserialize<MyType>(sequence.FirstSpan, _jsonSerializerOptions);
    }
    else
    {
        Span<byte> stackSpan = stackalloc byte[(int)sequence.Length];
        sequence.CopyTo(stackSpan);
        _result = JsonSerializer.Deserialize<MyType>(stackSpan, _jsonSerializerOptions);
    }
}
dbc
  • 104,963
  • 20
  • 228
  • 340
Petter T
  • 3,387
  • 2
  • 19
  • 31
  • 2
    side note: doing a `stackalloc` of something without checking the length is smallish first is kinda dangerous; if I *needed* to linearize data for some reason, I'd only use `stackalloc` after checking the length is, say, `<= 512`. For larger payloads **if you need to linearize** (which you don't here), you should probably rent an oversized array from `ArrayPool.Shared`, create a right-sized span over the oversized array, copy into *that* and consume it, and return the array to the pool – Marc Gravell Oct 24 '19 at 09:18
  • How about 2023? – huang Feb 10 '23 at 20:13

1 Answers1

3

Use the Utf8JsonReader type, which wraps a sequence (note: it can also wrap a span etc), and let it deal with the single/multi/etc segment concerns:

private void ProcessLine(ReadOnlySequence<byte> sequence)
{
    var reader = new Utf8JsonReader(sequence);
    _result = JsonSerializer.Deserialize<MyType>(ref reader);
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Unfortunately, the `Utf8JsonReader`-taking overloads of `JsonSerializer.Deserialize` also make a copy of the data (confirmed by documentation and source). That seems directly opposed to what the latest .NET Core versions are trying to achieve. Hopefully there will be overloads that take a `PipeReader` in the future. Any other ideas on how to do this without copying? – Timo Oct 25 '19 at 13:32
  • In regards to your answer, I was wondering: The sequence we get from a `PipeReader` is generally just one _part_ of the incoming byte stream, right? Can `JsonSerializer` deserialize the result sequence-by-sequence, without buffering the entire incoming byte stream at once? – Timo Oct 25 '19 at 13:35
  • @Timo most serializers currently work on entire payloads when it comes to pipelines, because a: fully async serialization/deserialization is *really really* hard (especially when dealing with spans), and b: the APIs to async-fetch (read) and async-flush (write) **aren't exposed** unless you ref System.IO.Pipelines directly, which undermines the idea of abstraction. For "b" there's an API proposal that might make it into .NET Core 3.1 – Marc Gravell Oct 25 '19 at 14:46
  • Thanks Marc. All the more reason to have this as part of .NET Core, I would say. :-) Do you have a link to the API proposal that you mentioned? – Timo Oct 26 '19 at 13:24
  • That is a neat API to look forward to! – Timo Oct 29 '19 at 09:00
  • 1
    Did you find that API proposal? How is it doing? – Benni Aug 13 '22 at 20:41