Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions src/RESPite/Buffers/CycleBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -712,11 +712,7 @@ public void Write(in ReadOnlySequence<byte> value)
{
if (value.IsSingleSegment)
{
#if NET
Write(value.FirstSpan);
#else
Write(value.First.Span);
#endif
}
else
{
Expand All @@ -727,11 +723,7 @@ static void WriteMultiSegment(ref CycleBuffer @this, in ReadOnlySequence<byte> v
{
foreach (var segment in value)
{
#if NET
@this.Write(value.FirstSpan);
#else
@this.Write(value.First.Span);
#endif
@this.Write(segment.Span);
}
}
}
Expand Down
4 changes: 0 additions & 4 deletions src/RESPite/Messages/RespFrameScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,7 @@ public OperationStatus TryRead(ref RespScanState state, in ReadOnlySequence<byte
{
if (!_pubsub & state.TotalBytes == 0 & data.IsSingleSegment)
{
#if NET
var status = TryFastRead(data.FirstSpan, ref state);
#else
var status = TryFastRead(data.First.Span, ref state);
#endif
if (status != UseReader) return status;
}

Expand Down
4 changes: 0 additions & 4 deletions src/RESPite/Messages/RespReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,11 +1065,7 @@ private void MovePastCurrent()

/// <inheritdoc cref="RespReader"/>
public RespReader(scoped in ReadOnlySequence<byte> value)
#if NET
: this(value.FirstSpan)
#else
: this(value.First.Span)
#endif
{
if (!value.IsSingleSegment)
{
Expand Down
33 changes: 33 additions & 0 deletions src/RESPite/Shared/FrameworkShims.Encoding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,39 @@ public static unsafe string GetString(this Encoding encoding, ReadOnlySpan<byte>
return encoding.GetString(bPtr, source.Length);
}
}

public static unsafe int GetChars(this Decoder decoder, ReadOnlySpan<byte> bytes, Span<char> chars, bool flush)
{
// empty input cannot flush any held-over bytes (verified on netfx), so 0 is correct either way
if (bytes.IsEmpty) return 0;
fixed (byte* bPtr = &MemoryMarshal.GetReference(bytes))
{
fixed (char* cPtr = &MemoryMarshal.GetReference(chars))
{
return decoder.GetChars(bPtr, bytes.Length, cPtr, chars.Length, flush);
}
}
}

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)
{
fixed (char* cPtr = &MemoryMarshal.GetReference(chars))
{
if (bytes.IsEmpty)
{
// a valid non-null pointer for the empty-input (flush-only) case
byte dummy = 0;
decoder.Convert(&dummy, 0, cPtr, chars.Length, flush, out bytesUsed, out charsUsed, out completed);
}
else
{
fixed (byte* bPtr = &MemoryMarshal.GetReference(bytes))
{
decoder.Convert(bPtr, bytes.Length, cPtr, chars.Length, flush, out bytesUsed, out charsUsed, out completed);
}
}
}
}
}
}
#endif
17 changes: 17 additions & 0 deletions src/RESPite/Shared/FrameworkShims.Sequence.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#if !NET
// ReSharper disable once CheckNamespace
namespace System.Buffers
{
internal static class FrameworkSequenceShims
{
extension<T>(scoped in ReadOnlySequence<T> sequence)
{
/// <summary>
/// Gets the first segment of the sequence as a span. On modern runtimes this is a BCL instance
/// property; this shim supplies it for older targets.
/// </summary>
public ReadOnlySpan<T> FirstSpan => sequence.First.Span;
}
}
}
#endif
97 changes: 97 additions & 0 deletions src/StackExchange.Redis/EncodingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Buffers;
using System.Text;

namespace StackExchange.Redis;

internal static class EncodingExtensions
{
// Above this length we stream through a Decoder; at or below it we linearize onto the stack and use the
// contiguous span overloads, avoiding the Decoder heap allocation for the common (small) case.
private const int MaxStackLinearizeBytes = 128;

// Note: there is no BCL Encoding.GetCharCount(in ReadOnlySequence<byte>) on any TFM (unlike GetChars,
// which the BCL provides on modern runtimes), so we supply it for all targets.
public static int GetCharCount(this Encoding encoding, in ReadOnlySequence<byte> seq)
{
// common case: a single segment can be measured directly, with no decoder state to track
if (seq.IsSingleSegment) return encoding.GetCharCount(seq.FirstSpan);

// small payloads: linearize onto the stack and measure contiguously - no Decoder allocation, and no
// glyph-straddles-a-boundary problem once the bytes are contiguous
long length = seq.Length;
if (length <= MaxStackLinearizeBytes)
{
Span<byte> linear = stackalloc byte[(int)length];
seq.CopyTo(linear);
return encoding.GetCharCount(linear);
}

// larger multi-segment: a multi-byte glyph can straddle a segment boundary, so we *must* decode with
// a stateful decoder rather than summing per-segment counts (which would over-count split glyphs).
// Note we cannot use Decoder.GetCharCount: unlike GetChars/Convert it does not carry partial-glyph
// state between calls, so it too over-counts. Decoder.Convert reports the chars produced without us
// having to keep them, so we decode into a small scratch buffer and discard the output, flushing on
// the final segment.
var decoder = encoding.GetDecoder();
Span<char> scratch = stackalloc char[128];
int count = 0;
var position = seq.Start;
bool have = seq.TryGet(ref position, out var current);
while (have)
{
var nextPosition = position;
bool haveNext = seq.TryGet(ref nextPosition, out var next);
var bytes = current.Span;
bool flush = !haveNext;
bool completed;
do
{
decoder.Convert(bytes, scratch, flush, out int bytesUsed, out int charsUsed, out completed);
count += charsUsed;
bytes = bytes.Slice(bytesUsed);
}
while (!bytes.IsEmpty || (flush && !completed));
current = next;
position = nextPosition;
have = haveNext;
}
return count;
}

#if NET461 || NET472 || NETSTANDARD2_0
// modern runtimes have a BCL Encoding.GetChars(in ReadOnlySequence<byte>, Span<char>); we only need to
// supply it for the older targets. The flush-on-final-segment logic mirrors GetCharCount above, which
// guarantees the two agree on the char count - so a buffer sized via GetCharCount cannot overflow here.
public static int GetChars(this Encoding encoding, in ReadOnlySequence<byte> seq, Span<char> chars)
{
if (seq.IsSingleSegment) return encoding.GetChars(seq.FirstSpan, chars);

// small payloads: linearize onto the stack and decode contiguously - no Decoder allocation
long length = seq.Length;
if (length <= MaxStackLinearizeBytes)
{
Span<byte> linear = stackalloc byte[(int)length];
seq.CopyTo(linear);
return encoding.GetChars(linear, chars);
}

var decoder = encoding.GetDecoder();
int total = 0;
var position = seq.Start;
bool have = seq.TryGet(ref position, out var current);
while (have)
{
var nextPosition = position;
bool haveNext = seq.TryGet(ref nextPosition, out var next);
int written = decoder.GetChars(current.Span, chars, flush: !haveNext);
chars = chars.Slice(written); // advance by chars *written*, not by bytes consumed
total += written;
current = next;
position = nextPosition;
have = haveNext;
}
return total;
}
#endif
}
7 changes: 4 additions & 3 deletions src/StackExchange.Redis/Format.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,11 @@ internal static bool TryParseEndPoint(string? addressWithPort, [NotNullWhen(true

internal static string GetString(ReadOnlySequence<byte> buffer)
{
if (buffer.IsSingleSegment) return GetString(buffer.First.Span);
if (buffer.IsSingleSegment) return GetString(buffer.FirstSpan);

var arr = ArrayPool<byte>.Shared.Rent(checked((int)buffer.Length));
var span = new Span<byte>(arr, 0, (int)buffer.Length);
var length = checked((int)buffer.Length);
var arr = ArrayPool<byte>.Shared.Rent(length);
var span = new Span<byte>(arr, 0, length);
buffer.CopyTo(span);
string s = GetString(span);
ArrayPool<byte>.Shared.Return(arr);
Expand Down
45 changes: 44 additions & 1 deletion src/StackExchange.Redis/MessageWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -102,6 +102,9 @@ internal static void WriteBulkString(in RedisValue value, IBufferWriter<byte> wr
case RedisValue.StorageType.MemoryManager or RedisValue.StorageType.ByteArray:
WriteUnifiedSpan(writer, value.RawSpan());
break;
case RedisValue.StorageType.Sequence:
WriteUnifiedSequenceIterator(writer, value.RawSequenceIterator());
break;
default:
throw new InvalidOperationException($"Unexpected {value.Type} value: '{value}'");
}
Expand Down Expand Up @@ -571,6 +574,46 @@ private static void WriteUnifiedSpan(IBufferWriter<byte> writer, ReadOnlySpan<by
}
}

/*
private static void WriteUnifiedSequence(IBufferWriter<byte> writer, in ReadOnlySequence<byte> value)
{
if (value.IsSingleSegment)
{
WriteUnifiedSpan(writer, value.FirstSpan);
}
else
{
// value.Length is a long, so reserve room for a 64-bit length ('$' + up to 20 digits + CRLF)
var span = writer.GetSpan(3 + Format.MaxInt64TextLen);
span[0] = (byte)'$';
int bytes = WriteRaw(span, value.Length, offset: 1);
writer.Advance(bytes);

foreach (var memory in value)
{
writer.Write(memory.Span);
}

WriteCrlf(writer);
}
}
*/

private static void WriteUnifiedSequenceIterator(IBufferWriter<byte> writer, ReadOnlySequenceSegmentIterator<byte> seq)
{
var span = writer.GetSpan(3 + Format.MaxInt32TextLen);
span[0] = (byte)'$';
int bytes = WriteRaw(span, seq.Length, offset: 1);
writer.Advance(bytes);

while (seq.TryNext(out var memory))
{
writer.Write(memory.Span);
}

WriteCrlf(writer);
}

private static int AppendToSpan(Span<byte> span, ReadOnlySpan<byte> value, int offset = 0)
{
offset = WriteRaw(span, value.Length, offset: offset);
Expand Down
4 changes: 0 additions & 4 deletions src/StackExchange.Redis/PhysicalConnection.Read.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,7 @@ private void OnResponseFrame(RespPrefix prefix, ReadOnlySequence<byte> payload)
{
if (payload.IsSingleSegment)
{
#if NET
OnResponseFrame(prefix, payload.FirstSpan, ref SharedNoLease);
#else
OnResponseFrame(prefix, payload.First.Span, ref SharedNoLease);
#endif
}
else
{
Expand Down
3 changes: 3 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
#nullable enable
static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.Buffers.ReadOnlySequence<byte> value) -> StackExchange.Redis.RedisValue
static StackExchange.Redis.RedisValue.implicit operator System.Buffers.ReadOnlySequence<byte>(StackExchange.Redis.RedisValue value) -> System.Buffers.ReadOnlySequence<byte>
[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(in System.Buffers.ReadOnlySequence<byte> value) -> StackExchange.Redis.ValueCondition
Loading
Loading