Skip to content

Commit 16bde35

Browse files
pairbitITikhonovmgravell
authored
support RedisValue backed by ReadOnlySequence (StackExchange#3112)
* Merge pull request #4 from pairbit/RedisValueSequence Redis value sequence * fix order * fix * PublicAPI.Unshipped * GetCharCount and GetMaxCharCount * add checking OverflowException * comment SequenceCompareTo * implement StartsWith * fix StartsWith * refact * GetHashCode * add Equals, CompareTo, StartsWith for ByteArray or MemoryManager * add ReadOnlySequenceIterator * refac * - GetHashCode - GetChars/GetCharCount - Simplify - normalize .First.Span * TryParse * mark WriteUnifiedSequence(ROS) as redundant for now (and fix 64-bit length) * CompareTo * add integration test (and fix CI) * polishing * avoid the string alloc in GetHashCode (when possible) --------- Co-authored-by: ITikhonov <ITikhonov@lanit.ru> Co-authored-by: Marc Gravell <marc.gravell@gmail.com>
1 parent 145540d commit 16bde35

20 files changed

Lines changed: 1139 additions & 91 deletions

src/RESPite/Buffers/CycleBuffer.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -712,11 +712,7 @@ public void Write(in ReadOnlySequence<byte> value)
712712
{
713713
if (value.IsSingleSegment)
714714
{
715-
#if NET
716715
Write(value.FirstSpan);
717-
#else
718-
Write(value.First.Span);
719-
#endif
720716
}
721717
else
722718
{
@@ -727,11 +723,7 @@ static void WriteMultiSegment(ref CycleBuffer @this, in ReadOnlySequence<byte> v
727723
{
728724
foreach (var segment in value)
729725
{
730-
#if NET
731-
@this.Write(value.FirstSpan);
732-
#else
733-
@this.Write(value.First.Span);
734-
#endif
726+
@this.Write(segment.Span);
735727
}
736728
}
737729
}

src/RESPite/Messages/RespFrameScanner.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,7 @@ public OperationStatus TryRead(ref RespScanState state, in ReadOnlySequence<byte
125125
{
126126
if (!_pubsub & state.TotalBytes == 0 & data.IsSingleSegment)
127127
{
128-
#if NET
129128
var status = TryFastRead(data.FirstSpan, ref state);
130-
#else
131-
var status = TryFastRead(data.First.Span, ref state);
132-
#endif
133129
if (status != UseReader) return status;
134130
}
135131

src/RESPite/Messages/RespReader.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,11 +1065,7 @@ private void MovePastCurrent()
10651065

10661066
/// <inheritdoc cref="RespReader"/>
10671067
public RespReader(scoped in ReadOnlySequence<byte> value)
1068-
#if NET
10691068
: this(value.FirstSpan)
1070-
#else
1071-
: this(value.First.Span)
1072-
#endif
10731069
{
10741070
if (!value.IsSingleSegment)
10751071
{

src/RESPite/Shared/FrameworkShims.Encoding.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@ public static unsafe string GetString(this Encoding encoding, ReadOnlySpan<byte>
4747
return encoding.GetString(bPtr, source.Length);
4848
}
4949
}
50+
51+
public static unsafe int GetChars(this Decoder decoder, ReadOnlySpan<byte> bytes, Span<char> chars, bool flush)
52+
{
53+
// empty input cannot flush any held-over bytes (verified on netfx), so 0 is correct either way
54+
if (bytes.IsEmpty) return 0;
55+
fixed (byte* bPtr = &MemoryMarshal.GetReference(bytes))
56+
{
57+
fixed (char* cPtr = &MemoryMarshal.GetReference(chars))
58+
{
59+
return decoder.GetChars(bPtr, bytes.Length, cPtr, chars.Length, flush);
60+
}
61+
}
62+
}
63+
64+
public static unsafe void Convert(this Decoder decoder, ReadOnlySpan<byte> bytes, Span<char> chars, bool flush, out int bytesUsed, out int charsUsed, out bool completed)
65+
{
66+
fixed (char* cPtr = &MemoryMarshal.GetReference(chars))
67+
{
68+
if (bytes.IsEmpty)
69+
{
70+
// a valid non-null pointer for the empty-input (flush-only) case
71+
byte dummy = 0;
72+
decoder.Convert(&dummy, 0, cPtr, chars.Length, flush, out bytesUsed, out charsUsed, out completed);
73+
}
74+
else
75+
{
76+
fixed (byte* bPtr = &MemoryMarshal.GetReference(bytes))
77+
{
78+
decoder.Convert(bPtr, bytes.Length, cPtr, chars.Length, flush, out bytesUsed, out charsUsed, out completed);
79+
}
80+
}
81+
}
82+
}
5083
}
5184
}
5285
#endif
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#if !NET
2+
// ReSharper disable once CheckNamespace
3+
namespace System.Buffers
4+
{
5+
internal static class FrameworkSequenceShims
6+
{
7+
extension<T>(scoped in ReadOnlySequence<T> sequence)
8+
{
9+
/// <summary>
10+
/// Gets the first segment of the sequence as a span. On modern runtimes this is a BCL instance
11+
/// property; this shim supplies it for older targets.
12+
/// </summary>
13+
public ReadOnlySpan<T> FirstSpan => sequence.First.Span;
14+
}
15+
}
16+
}
17+
#endif
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Text;
4+
5+
namespace StackExchange.Redis;
6+
7+
internal static class EncodingExtensions
8+
{
9+
// Above this length we stream through a Decoder; at or below it we linearize onto the stack and use the
10+
// contiguous span overloads, avoiding the Decoder heap allocation for the common (small) case.
11+
private const int MaxStackLinearizeBytes = 128;
12+
13+
// Note: there is no BCL Encoding.GetCharCount(in ReadOnlySequence<byte>) on any TFM (unlike GetChars,
14+
// which the BCL provides on modern runtimes), so we supply it for all targets.
15+
public static int GetCharCount(this Encoding encoding, in ReadOnlySequence<byte> seq)
16+
{
17+
// common case: a single segment can be measured directly, with no decoder state to track
18+
if (seq.IsSingleSegment) return encoding.GetCharCount(seq.FirstSpan);
19+
20+
// small payloads: linearize onto the stack and measure contiguously - no Decoder allocation, and no
21+
// glyph-straddles-a-boundary problem once the bytes are contiguous
22+
long length = seq.Length;
23+
if (length <= MaxStackLinearizeBytes)
24+
{
25+
Span<byte> linear = stackalloc byte[(int)length];
26+
seq.CopyTo(linear);
27+
return encoding.GetCharCount(linear);
28+
}
29+
30+
// larger multi-segment: a multi-byte glyph can straddle a segment boundary, so we *must* decode with
31+
// a stateful decoder rather than summing per-segment counts (which would over-count split glyphs).
32+
// Note we cannot use Decoder.GetCharCount: unlike GetChars/Convert it does not carry partial-glyph
33+
// state between calls, so it too over-counts. Decoder.Convert reports the chars produced without us
34+
// having to keep them, so we decode into a small scratch buffer and discard the output, flushing on
35+
// the final segment.
36+
var decoder = encoding.GetDecoder();
37+
Span<char> scratch = stackalloc char[128];
38+
int count = 0;
39+
var position = seq.Start;
40+
bool have = seq.TryGet(ref position, out var current);
41+
while (have)
42+
{
43+
var nextPosition = position;
44+
bool haveNext = seq.TryGet(ref nextPosition, out var next);
45+
var bytes = current.Span;
46+
bool flush = !haveNext;
47+
bool completed;
48+
do
49+
{
50+
decoder.Convert(bytes, scratch, flush, out int bytesUsed, out int charsUsed, out completed);
51+
count += charsUsed;
52+
bytes = bytes.Slice(bytesUsed);
53+
}
54+
while (!bytes.IsEmpty || (flush && !completed));
55+
current = next;
56+
position = nextPosition;
57+
have = haveNext;
58+
}
59+
return count;
60+
}
61+
62+
#if NET461 || NET472 || NETSTANDARD2_0
63+
// modern runtimes have a BCL Encoding.GetChars(in ReadOnlySequence<byte>, Span<char>); we only need to
64+
// supply it for the older targets. The flush-on-final-segment logic mirrors GetCharCount above, which
65+
// guarantees the two agree on the char count - so a buffer sized via GetCharCount cannot overflow here.
66+
public static int GetChars(this Encoding encoding, in ReadOnlySequence<byte> seq, Span<char> chars)
67+
{
68+
if (seq.IsSingleSegment) return encoding.GetChars(seq.FirstSpan, chars);
69+
70+
// small payloads: linearize onto the stack and decode contiguously - no Decoder allocation
71+
long length = seq.Length;
72+
if (length <= MaxStackLinearizeBytes)
73+
{
74+
Span<byte> linear = stackalloc byte[(int)length];
75+
seq.CopyTo(linear);
76+
return encoding.GetChars(linear, chars);
77+
}
78+
79+
var decoder = encoding.GetDecoder();
80+
int total = 0;
81+
var position = seq.Start;
82+
bool have = seq.TryGet(ref position, out var current);
83+
while (have)
84+
{
85+
var nextPosition = position;
86+
bool haveNext = seq.TryGet(ref nextPosition, out var next);
87+
int written = decoder.GetChars(current.Span, chars, flush: !haveNext);
88+
chars = chars.Slice(written); // advance by chars *written*, not by bytes consumed
89+
total += written;
90+
current = next;
91+
position = nextPosition;
92+
have = haveNext;
93+
}
94+
return total;
95+
}
96+
#endif
97+
}

src/StackExchange.Redis/Format.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,11 @@ internal static bool TryParseEndPoint(string? addressWithPort, [NotNullWhen(true
394394

395395
internal static string GetString(ReadOnlySequence<byte> buffer)
396396
{
397-
if (buffer.IsSingleSegment) return GetString(buffer.First.Span);
397+
if (buffer.IsSingleSegment) return GetString(buffer.FirstSpan);
398398

399-
var arr = ArrayPool<byte>.Shared.Rent(checked((int)buffer.Length));
400-
var span = new Span<byte>(arr, 0, (int)buffer.Length);
399+
var length = checked((int)buffer.Length);
400+
var arr = ArrayPool<byte>.Shared.Rent(length);
401+
var span = new Span<byte>(arr, 0, length);
401402
buffer.CopyTo(span);
402403
string s = GetString(span);
403404
ArrayPool<byte>.Shared.Return(arr);

src/StackExchange.Redis/MessageWriter.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Buffers;
33
using System.Diagnostics;
44
using System.Runtime.CompilerServices;
@@ -102,6 +102,9 @@ internal static void WriteBulkString(in RedisValue value, IBufferWriter<byte> wr
102102
case RedisValue.StorageType.MemoryManager or RedisValue.StorageType.ByteArray:
103103
WriteUnifiedSpan(writer, value.RawSpan());
104104
break;
105+
case RedisValue.StorageType.Sequence:
106+
WriteUnifiedSequenceIterator(writer, value.RawSequenceIterator());
107+
break;
105108
default:
106109
throw new InvalidOperationException($"Unexpected {value.Type} value: '{value}'");
107110
}
@@ -571,6 +574,46 @@ private static void WriteUnifiedSpan(IBufferWriter<byte> writer, ReadOnlySpan<by
571574
}
572575
}
573576

577+
/*
578+
private static void WriteUnifiedSequence(IBufferWriter<byte> writer, in ReadOnlySequence<byte> value)
579+
{
580+
if (value.IsSingleSegment)
581+
{
582+
WriteUnifiedSpan(writer, value.FirstSpan);
583+
}
584+
else
585+
{
586+
// value.Length is a long, so reserve room for a 64-bit length ('$' + up to 20 digits + CRLF)
587+
var span = writer.GetSpan(3 + Format.MaxInt64TextLen);
588+
span[0] = (byte)'$';
589+
int bytes = WriteRaw(span, value.Length, offset: 1);
590+
writer.Advance(bytes);
591+
592+
foreach (var memory in value)
593+
{
594+
writer.Write(memory.Span);
595+
}
596+
597+
WriteCrlf(writer);
598+
}
599+
}
600+
*/
601+
602+
private static void WriteUnifiedSequenceIterator(IBufferWriter<byte> writer, ReadOnlySequenceSegmentIterator<byte> seq)
603+
{
604+
var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
605+
span[0] = (byte)'$';
606+
int bytes = WriteRaw(span, seq.Length, offset: 1);
607+
writer.Advance(bytes);
608+
609+
while (seq.TryNext(out var memory))
610+
{
611+
writer.Write(memory.Span);
612+
}
613+
614+
WriteCrlf(writer);
615+
}
616+
574617
private static int AppendToSpan(Span<byte> span, ReadOnlySpan<byte> value, int offset = 0)
575618
{
576619
offset = WriteRaw(span, value.Length, offset: offset);

src/StackExchange.Redis/PhysicalConnection.Read.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,7 @@ private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence<byte> payload)
392392
{
393393
if (payload.IsSingleSegment)
394394
{
395-
#if NET
396395
OnResponseFrame(prefix, payload.FirstSpan, ref SharedNoLease);
397-
#else
398-
OnResponseFrame(prefix, payload.First.Span, ref SharedNoLease);
399-
#endif
400396
}
401397
else
402398
{
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
#nullable enable
2+
static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.Buffers.ReadOnlySequence<byte> value) -> StackExchange.Redis.RedisValue
3+
static StackExchange.Redis.RedisValue.implicit operator System.Buffers.ReadOnlySequence<byte>(StackExchange.Redis.RedisValue value) -> System.Buffers.ReadOnlySequence<byte>
4+
[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(in System.Buffers.ReadOnlySequence<byte> value) -> StackExchange.Redis.ValueCondition

0 commit comments

Comments
 (0)