diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index 779a8153cac..abd6631955c 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -3805,6 +3805,7 @@ namespace Akka.IO public Akka.IO.ByteString Slice(int index) { } public Akka.IO.ByteString Slice(int index, int count) { } public byte[] ToArray() { } + public System.Buffers.ReadOnlySequence ToReadOnlySequence() { } public System.ReadOnlySpan ToReadOnlySpan() { } public override string ToString() { } public string ToString(System.Text.Encoding encoding) { } @@ -3815,6 +3816,11 @@ namespace Akka.IO public static Akka.IO.ByteString op_Explicit(byte[] bytes) { } public static byte[] op_Explicit(Akka.IO.ByteString byteString) { } public static bool !=(Akka.IO.ByteString x, Akka.IO.ByteString y) { } + [System.Diagnostics.DebuggerDisplayAttribute("(RunningIndex = {RunningIndex}, Length = {Memory.Length})}")] + public sealed class ByteStringReadOnlySequenceSegment : System.Buffers.ReadOnlySequenceSegment + { + public static System.Buffers.ReadOnlySequence CreateSequence(Akka.IO.ByteString bs) { } + } } [Akka.Annotations.InternalApiAttribute()] public class ConnectException : System.Exception diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt index 52408af9289..0b0b76e7f34 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt @@ -3795,6 +3795,7 @@ namespace Akka.IO public Akka.IO.ByteString Slice(int index) { } public Akka.IO.ByteString Slice(int index, int count) { } public byte[] ToArray() { } + public System.Buffers.ReadOnlySequence ToReadOnlySequence() { } public System.ReadOnlySpan ToReadOnlySpan() { } public override string ToString() { } public string ToString(System.Text.Encoding encoding) { } @@ -3805,6 +3806,11 @@ namespace Akka.IO public static Akka.IO.ByteString op_Explicit(byte[] bytes) { } public static byte[] op_Explicit(Akka.IO.ByteString byteString) { } public static bool !=(Akka.IO.ByteString x, Akka.IO.ByteString y) { } + [System.Diagnostics.DebuggerDisplayAttribute("(RunningIndex = {RunningIndex}, Length = {Memory.Length})}")] + public sealed class ByteStringReadOnlySequenceSegment : System.Buffers.ReadOnlySequenceSegment + { + public static System.Buffers.ReadOnlySequence CreateSequence(Akka.IO.ByteString bs) { } + } } [Akka.Annotations.InternalApiAttribute()] public class ConnectException : System.Exception diff --git a/src/core/Akka.Tests/Util/ByteStringSpec.cs b/src/core/Akka.Tests/Util/ByteStringSpec.cs index 053cf6bbd43..cc127b784ab 100644 --- a/src/core/Akka.Tests/Util/ByteStringSpec.cs +++ b/src/core/Akka.Tests/Util/ByteStringSpec.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Buffers; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -97,6 +98,64 @@ public void A_ByteString_ToReadOnlySpan_compacted_must_have_identical_memory_ref var concatSpan4 = compacted.ToReadOnlySpan(); (concatSpan3 == concatSpan4).Should().BeTrue(); } + + [Fact] + public void A_ByteString_ToReadOnlySequence_must_have_correct_size() + { + Prop.ForAll((ByteString a, ByteString b) => + { + a.ToReadOnlySequence().Length.Should().Be(a.Count); + b.ToReadOnlySequence().Length.Should().Be(b.Count); + var concat = a + b; + var spanConcat = concat.ToReadOnlySequence(); + return spanConcat.Length == concat.Count; + }).QuickCheckThrowOnFailure(); + } + + [Fact] + public void A_ByteString_ToReadOnlySequence_compacted_must_have_identical_contents_to_original() + { + var bytesA = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + var bytesB = new byte[] { 10, 11, 12, 13, 14, 15, 16, 17, 18 }; + var a = ByteString.FromBytes(bytesA); + var b = ByteString.FromBytes(bytesB); + + var concat = a + b; + + var concatSpan = concat.ToReadOnlySequence(); + var concatSpan2 = concat.ToReadOnlySequence(); + // not compact, returns a new span + (concatSpan.ToArray().SequenceEqual(concatSpan2.ToArray())).Should().BeTrue(); + + // compact, will return same span + var compacted = concat.Compact(); + var concatSpan3 = compacted.ToReadOnlySequence(); + (concatSpan.ToArray().SequenceEqual(concatSpan3.ToArray())).Should().BeTrue(); + } + + [Fact] + public void A_ByteString_ToReadOnlySequence_compacted_must_have_identical_contents_to_original_with_many_Segments() + { + var bytesA = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + var bytesB = new byte[] { 10, 11, 12, 13, 14, 15, 16, 17, 18 }; + var bytesC = new byte[] { 19, 20, 21, 22, 23, 24, 25, 26, 27 }; + var a = ByteString.FromBytes(bytesA); + var b = ByteString.FromBytes(bytesB); + var c = ByteString.FromBytes(bytesC); + var concat = a + b +c; + + var concatSpan = concat.ToReadOnlySequence(); + var concatSpan2 = concat.ToReadOnlySequence(); + // not compact, returns a new span + (concatSpan.ToArray().SequenceEqual(concatSpan2.ToArray())).Should().BeTrue(); + + // compact, will return same span + var compacted = concat.Compact(); + var concatSpan3 = compacted.ToReadOnlySequence(); + (concatSpan.ToArray().SequenceEqual(concatSpan3.ToArray())).Should().BeTrue(); + + (concatSpan.ToArray().SequenceEqual(compacted.ToArray())).Should().BeTrue(); + } [Fact] public void A_ByteString_must_have_correct_size_when_slicing_from_index() diff --git a/src/core/Akka/Util/ByteString.cs b/src/core/Akka/Util/ByteString.cs index 46436cfc747..7cd76fccfee 100644 --- a/src/core/Akka/Util/ByteString.cs +++ b/src/core/Akka/Util/ByteString.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -28,6 +29,40 @@ namespace Akka.IO [DebuggerDisplay("(Count = {_count}, Buffers = {_buffers})")] public sealed class ByteString : IEquatable, IEnumerable { + /// + /// A to encapsulate + /// Parts of a in a + /// + [DebuggerDisplay("(RunningIndex = {RunningIndex}, Length = {Memory.Length})}")] + public sealed class ByteStringReadOnlySequenceSegment : ReadOnlySequenceSegment + { + private ByteStringReadOnlySequenceSegment(ReadOnlyMemory memory, long runningIndex) + { + Memory = memory; + RunningIndex = runningIndex; + } + + /// + /// This is here because + /// has predefined properties with Protected Setters. + /// + public static ReadOnlySequence CreateSequence(ByteString bs) + { + var bArr = bs._buffers; + var first = new ByteStringReadOnlySequenceSegment(bArr[0], 0); + var prior = first; + for (int i = 1; i < bArr.Length; i++) + { + var item = bArr[i]; + var curr = new ByteStringReadOnlySequenceSegment(item, prior.RunningIndex + prior.Memory.Length); + prior.Next = curr; + prior = curr; + } + + return new ReadOnlySequence(first, 0, prior, prior.Memory.Length); + } + } + #region creation methods /// @@ -528,6 +563,33 @@ public ReadOnlySpan ToReadOnlySpan() return new ReadOnlySpan(ToArray()); } + + /// + /// Returns a of over the contents of this ByteString. + /// This is not a copying operation and zero-alloc when ByteString is compact + /// Otherwise N s will be allocated + /// Where N is the number of arrays currently internally held by the ByteString + /// + /// + /// A of + /// over the data in the + /// + public ReadOnlySequence ToReadOnlySequence() + { + if (_count == 0) + { + return ReadOnlySequence.Empty; + } + else if (_buffers.Length == 1) + { + //Happy path, we can just pass ArraySegment here and avoid alloc. + return new ReadOnlySequence(_buffers[0]); + } + else + { + return ByteStringReadOnlySequenceSegment.CreateSequence(this); + } + } /// /// Appends at the tail