From ab73d617a2f9c3b979ae4d32da5c89b3e60acd4d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 17:26:23 +0200 Subject: [PATCH 01/10] Added IBufferWriterStream type --- .../Streams/IBufferWriterStream.Memory.cs | 76 ++++++++ .../Streams/IBufferWriterStream.cs | 169 ++++++++++++++++++ .../Streams/MemoryStream.ThrowExceptions.cs | 29 +-- .../Streams/MemoryStream.Validate.cs | 2 +- .../Streams/MemoryStream{TSource}.cs | 2 +- 5 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs create mode 100644 Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs new file mode 100644 index 00000000000..eeb9e78b9e9 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if SPAN_RUNTIME_SUPPORT + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + internal sealed partial class IBufferWriterStream + { + /// + public override void CopyTo(Stream destination, int bufferSize) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + try + { + Write(buffer.Span); + + return default; + } + catch (OperationCanceledException e) + { + return new ValueTask(Task.FromCanceled(e.CancellationToken)); + } + catch (Exception e) + { + return new ValueTask(Task.FromException(e)); + } + } + + /// + public override int Read(Span buffer) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void Write(ReadOnlySpan buffer) + { + MemoryStream.ValidateDisposed(this.disposed); + + Span destination = this.bufferWriter.GetSpan(buffer.Length); + + if (!buffer.TryCopyTo(destination)) + { + MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.bufferWriter.Advance(buffer.Length); + } + } +} + +#endif diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs new file mode 100644 index 00000000000..4fca743297b --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.HighPerformance.Streams +{ + /// + /// A implementation wrapping an instance. + /// + internal sealed partial class IBufferWriterStream : Stream + { + /// + /// The target instance to use. + /// + private readonly IBufferWriter bufferWriter; + + /// + /// Indicates whether or not the current instance has been disposed + /// + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The target instance to use. + public IBufferWriterStream(IBufferWriter bufferWriter) + { + this.bufferWriter = bufferWriter; + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public override long Length => throw MemoryStream.GetNotSupportedException(); + + /// + public override long Position + { + get => throw MemoryStream.GetNotSupportedException(); + set => throw MemoryStream.GetNotSupportedException(); + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + /// + public override Task ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override Task WriteAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(buffer, offset, count); + + return Task.CompletedTask; + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override int Read(byte[]? buffer, int offset, int count) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override int ReadByte() + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public override void Write(byte[]? buffer, int offset, int count) + { + MemoryStream.ValidateDisposed(this.disposed); + MemoryStream.ValidateBuffer(buffer, offset, count); + + Span + source = buffer.AsSpan(offset, count), + destination = this.bufferWriter.GetSpan(count); + + if (!source.TryCopyTo(destination)) + { + MemoryStream.ThrowArgumentExceptionForEndOfStreamOnWrite(); + } + + this.bufferWriter.Advance(count); + } + + /// + public override void WriteByte(byte value) + { + MemoryStream.ValidateDisposed(this.disposed); + + this.bufferWriter.GetSpan(1)[0] = value; + } + + /// + protected override void Dispose(bool disposing) + { + this.disposed = true; + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs index cdfc24112d5..2d316df2242 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.ThrowExceptions.cs @@ -13,19 +13,28 @@ namespace Microsoft.Toolkit.HighPerformance.Streams internal static partial class MemoryStream { /// - /// Throws an when trying to write too many bytes to the target stream. + /// Gets a standard instance for a stream. /// - public static void ThrowArgumentExceptionForEndOfStreamOnWrite() + /// A with the standard text. + public static Exception GetNotSupportedException() { - throw new ArgumentException("The current stream can't contain the requested input data."); + return new NotSupportedException("The requested operation is not supported for this stream."); + } + + /// + /// Throws a when trying to perform a not supported operation. + /// + public static void ThrowNotSupportedException() + { + throw GetNotSupportedException(); } /// - /// Throws a when trying to set the length of the stream. + /// Throws an when trying to write too many bytes to the target stream. /// - public static void ThrowNotSupportedExceptionForSetLength() + public static void ThrowArgumentExceptionForEndOfStreamOnWrite() { - throw new NotSupportedException("Setting the length is not supported for this stream."); + throw new ArgumentException("The current stream can't contain the requested input data."); } /// @@ -77,14 +86,6 @@ private static void ThrowArgumentExceptionForLength() throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer"); } - /// - /// Throws a when trying to write on a readonly stream. - /// - private static void ThrowNotSupportedExceptionForCanWrite() - { - throw new NotSupportedException("The current stream doesn't support writing."); - } - /// /// Throws an when using a disposed instance. /// diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs index 9cd0ff62936..7c61830d956 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream.Validate.cs @@ -64,7 +64,7 @@ public static void ValidateCanWrite(bool canWrite) { if (!canWrite) { - ThrowNotSupportedExceptionForCanWrite(); + ThrowNotSupportedException(); } } diff --git a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs index b0dcd8bc908..0243dbd60d5 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/MemoryStream{TSource}.cs @@ -220,7 +220,7 @@ public sealed override long Seek(long offset, SeekOrigin origin) /// public sealed override void SetLength(long value) { - MemoryStream.ThrowNotSupportedExceptionForSetLength(); + throw MemoryStream.GetNotSupportedException(); } /// From bcadbd72c025d03750466bec65a066e42f0fbfa3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 17:30:44 +0200 Subject: [PATCH 02/10] Added IBufferWriter.AsStream extension --- .../Extensions/IBufferWriterExtensions.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index 67c30579beb..1d295dfca12 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -4,8 +4,11 @@ using System; using System.Buffers; +using System.Diagnostics.Contracts; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Streams; namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -14,6 +17,19 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions /// public static class IBufferWriterExtensions { + /// + /// Returns a that can be used to write to a target of instance. + /// + /// The target of instance. + /// A wrapping and writing data to its underlying buffer. + /// The returned can only be written to and does not support seeking. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this IBufferWriter writer) + { + return new IBufferWriterStream(writer); + } + /// /// Writes a value of a specified type into a target instance. /// From dc13fb9ae20584ca50443d032a0ac54ffe856e6f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 17:56:42 +0200 Subject: [PATCH 03/10] Added unit tests for IBufferWriterStream --- .../Streams/Test_IBufferWriterStream.cs | 149 ++++++++++++++++++ .../Streams/Test_MemoryStream.cs | 2 +- ...UnitTests.HighPerformance.Shared.projitems | 1 + 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs new file mode 100644 index 00000000000..7d0c7beeef9 --- /dev/null +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.HighPerformance.Streams +{ + [TestClass] + public class Test_IBufferWriterStream + { + [TestCategory("IBufferWriterStream")] + [TestMethod] + public void Test_IBufferWriterStream_Lifecycle() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + + Stream stream = ((IBufferWriter)writer).AsStream(); + + Assert.IsTrue(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + + Assert.ThrowsException(() => stream.Length); + Assert.ThrowsException(() => stream.Position); + + stream.Dispose(); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + Assert.ThrowsException(() => stream.Length); + Assert.ThrowsException(() => stream.Position); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public void Test_IBufferWriterStream_Write_Array() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + byte[] data = Test_MemoryStream.CreateRandomData(64); + + stream.Write(data, 0, data.Length); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data)); + + Assert.ThrowsException(() => stream.Write(null, 0, 10)); + Assert.ThrowsException(() => stream.Write(data, -1, 10)); + Assert.ThrowsException(() => stream.Write(data, 200, 10)); + Assert.ThrowsException(() => stream.Write(data, 0, -24)); + Assert.ThrowsException(() => stream.Write(data, 0, 200)); + + stream.Dispose(); + + Assert.ThrowsException(() => stream.Write(data, 0, data.Length)); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public async Task Test_IBufferWriterStream_WriteAsync_Array() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + byte[] data = Test_MemoryStream.CreateRandomData(64); + + await stream.WriteAsync(data, 0, data.Length); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data)); + + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(null, 0, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, -1, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 200, 10)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, -24)); + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, 200)); + + stream.Dispose(); + + await Assert.ThrowsExceptionAsync(() => stream.WriteAsync(data, 0, data.Length)); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1500", Justification = "Array initialization")] + public void Test_IBufferWriterStream_WriteByte() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + ReadOnlySpan data = stackalloc byte[] { 1, 128, 255, 32 }; + + foreach (var item in data.Enumerate()) + { + Assert.AreEqual(writer.WrittenCount, item.Index); + + stream.WriteByte(item.Value); + } + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(data.SequenceEqual(writer.WrittenSpan)); + + Assert.ThrowsException(() => stream.ReadByte()); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public void Test_IBufferWriterStream_Write_Span() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + Memory data = Test_MemoryStream.CreateRandomData(64); + + // This will use the extension when on .NET Standard 2.0, + // as the Stream class doesn't have Spam or Memory + // public APIs there. This is the case eg. on UWP as well. + stream.Write(data.Span); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan)); + } + + [TestCategory("IBufferWriterStream")] + [TestMethod] + public async Task Test_IBufferWriterStream_WriteAsync_Memory() + { + ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + Stream stream = ((IBufferWriter)writer).AsStream(); + + Memory data = Test_MemoryStream.CreateRandomData(64); + + await stream.WriteAsync(data); + + Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(writer.WrittenSpan)); + } + } +} diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs index 8a9e19111cb..fefcb9094a2 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_MemoryStream.cs @@ -236,7 +236,7 @@ public async Task Test_MemoryStream_ReadWriteAsync_Memory() /// The number of array items to create. /// The returned random array. [Pure] - private static byte[] CreateRandomData(int count) + internal static byte[] CreateRandomData(int count) { var random = new Random(DateTime.Now.Ticks.GetHashCode()); diff --git a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems index ef69154b89f..12c87de070b 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems +++ b/UnitTests/UnitTests.HighPerformance.Shared/UnitTests.HighPerformance.Shared.projitems @@ -39,6 +39,7 @@ + From 23c8abb72d81b0c8a5cb9125e0e162e0b8569acc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 17:58:51 +0200 Subject: [PATCH 04/10] Fixed a build error in a unit test --- .../Buffers/Test_ArrayPoolBufferWriter{T}.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs index e6770e01c59..08d30b2190f 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -159,7 +160,7 @@ public void Test_ArrayPoolBufferWriterOfT_AsStream() Assert.AreEqual(writer.WrittenCount, data.Length); - Stream stream = writer.AsStream(); + Stream stream = ((IMemoryOwner)writer).AsStream(); Assert.AreEqual(stream.Length, data.Length); From 25d291cad4ff84ffa53227d38c8664c710939615 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Oct 2020 23:00:54 +0200 Subject: [PATCH 05/10] Fixed a bug in IBufferWriterStream.WriteByte --- .../Streams/IBufferWriterStream.cs | 2 ++ .../Streams/Test_IBufferWriterStream.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs index 4fca743297b..d3fb4fac92d 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs @@ -158,6 +158,8 @@ public override void WriteByte(byte value) MemoryStream.ValidateDisposed(this.disposed); this.bufferWriter.GetSpan(1)[0] = value; + + this.bufferWriter.Advance(1); } /// diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs index 7d0c7beeef9..a5dabe764dc 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs @@ -24,9 +24,9 @@ public void Test_IBufferWriterStream_Lifecycle() Stream stream = ((IBufferWriter)writer).AsStream(); - Assert.IsTrue(stream.CanRead); + Assert.IsFalse(stream.CanRead); Assert.IsFalse(stream.CanSeek); - Assert.IsFalse(stream.CanWrite); + Assert.IsTrue(stream.CanWrite); Assert.ThrowsException(() => stream.Length); Assert.ThrowsException(() => stream.Position); From 62eaaa4a04d350fbc308c0263c4d828ccdd025f8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 6 Oct 2020 13:41:57 +0200 Subject: [PATCH 06/10] Removed callvirt-s in BufferWriterStream and AsStream cast --- .../ArrayPoolBufferWriterExtensions.cs | 32 ++++++++++++ .../Extensions/IBufferWriterExtensions.cs | 7 +-- ...=> IBufferWriterStream{TWriter}.Memory.cs} | 4 +- ...eam.cs => IBufferWriterStream{TWriter}.cs} | 12 +++-- .../Streams/Sources/ArrayBufferWriterOwner.cs | 52 +++++++++++++++++++ .../Streams/Sources/IBufferWriterOwner.cs | 52 +++++++++++++++++++ 6 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs rename Microsoft.Toolkit.HighPerformance/Streams/{IBufferWriterStream.Memory.cs => IBufferWriterStream{TWriter}.Memory.cs} (94%) rename Microsoft.Toolkit.HighPerformance/Streams/{IBufferWriterStream.cs => IBufferWriterStream{TWriter}.cs} (92%) create mode 100644 Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs create mode 100644 Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs new file mode 100644 index 00000000000..21026113957 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Extensions/ArrayPoolBufferWriterExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.IO; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers; +using Microsoft.Toolkit.HighPerformance.Streams; +using Microsoft.Toolkit.HighPerformance.Streams.Sources; + +namespace Microsoft.Toolkit.HighPerformance.Extensions +{ + /// + /// Helpers for working with the type. + /// + public static class ArrayPoolBufferWriterExtensions + { + /// + /// Returns a that can be used to write to a target an of instance. + /// + /// The target instance. + /// A wrapping and writing data to its underlying buffer. + /// The returned can only be written to and does not support seeking. + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this ArrayPoolBufferWriter writer) + { + return new IBufferWriterStream(new ArrayBufferWriterOwner(writer)); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index 1d295dfca12..e6ffb06f95a 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.Toolkit.HighPerformance.Streams; +using Microsoft.Toolkit.HighPerformance.Streams.Sources; namespace Microsoft.Toolkit.HighPerformance.Extensions { @@ -18,16 +19,16 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions public static class IBufferWriterExtensions { /// - /// Returns a that can be used to write to a target of instance. + /// Returns a that can be used to write to a target an of instance. /// - /// The target of instance. + /// The target instance. /// A wrapping and writing data to its underlying buffer. /// The returned can only be written to and does not support seeking. [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Stream AsStream(this IBufferWriter writer) { - return new IBufferWriterStream(writer); + return new IBufferWriterStream(new IBufferWriterOwner(writer)); } /// diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs similarity index 94% rename from Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs rename to Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs index eeb9e78b9e9..f9b431fd904 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.Memory.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.Memory.cs @@ -11,8 +11,8 @@ namespace Microsoft.Toolkit.HighPerformance.Streams { - /// - internal sealed partial class IBufferWriterStream + /// + internal sealed partial class IBufferWriterStream { /// public override void CopyTo(Stream destination, int bufferSize) diff --git a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs similarity index 92% rename from Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs rename to Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs index d3fb4fac92d..2be38aa9761 100644 --- a/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream.cs +++ b/Microsoft.Toolkit.HighPerformance/Streams/IBufferWriterStream{TWriter}.cs @@ -14,12 +14,14 @@ namespace Microsoft.Toolkit.HighPerformance.Streams /// /// A implementation wrapping an instance. /// - internal sealed partial class IBufferWriterStream : Stream + /// The type of buffer writer to use. + internal sealed partial class IBufferWriterStream : Stream + where TWriter : struct, IBufferWriter { /// - /// The target instance to use. + /// The target instance to use. /// - private readonly IBufferWriter bufferWriter; + private readonly TWriter bufferWriter; /// /// Indicates whether or not the current instance has been disposed @@ -27,10 +29,10 @@ internal sealed partial class IBufferWriterStream : Stream private bool disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The target instance to use. - public IBufferWriterStream(IBufferWriter bufferWriter) + public IBufferWriterStream(TWriter bufferWriter) { this.bufferWriter = bufferWriter; } diff --git a/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs b/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs new file mode 100644 index 00000000000..0a534dc2af6 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/Sources/ArrayBufferWriterOwner.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers; + +namespace Microsoft.Toolkit.HighPerformance.Streams.Sources +{ + /// + /// An implementation wrapping an instance. + /// + internal readonly struct ArrayBufferWriterOwner : IBufferWriter + { + /// + /// The wrapped array. + /// + private readonly ArrayPoolBufferWriter writer; + + /// + /// Initializes a new instance of the struct. + /// + /// The wrapped instance. + public ArrayBufferWriterOwner(ArrayPoolBufferWriter writer) + { + this.writer = writer; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + this.writer.Advance(count); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Memory GetMemory(int sizeHint = 0) + { + return this.writer.GetMemory(sizeHint); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int sizeHint = 0) + { + return this.writer.GetSpan(sizeHint); + } + } +} diff --git a/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs b/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs new file mode 100644 index 00000000000..2580037f6d9 --- /dev/null +++ b/Microsoft.Toolkit.HighPerformance/Streams/Sources/IBufferWriterOwner.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.HighPerformance.Buffers; + +namespace Microsoft.Toolkit.HighPerformance.Streams.Sources +{ + /// + /// An implementation wrapping an instance. + /// + internal readonly struct IBufferWriterOwner : IBufferWriter + { + /// + /// The wrapped array. + /// + private readonly IBufferWriter writer; + + /// + /// Initializes a new instance of the struct. + /// + /// The wrapped instance. + public IBufferWriterOwner(IBufferWriter writer) + { + this.writer = writer; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + this.writer.Advance(count); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Memory GetMemory(int sizeHint = 0) + { + return this.writer.GetMemory(sizeHint); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpan(int sizeHint = 0) + { + return this.writer.GetSpan(sizeHint); + } + } +} From b93f42f1cf93b415300991dc7a821f4ca83c73ee Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 20 Oct 2020 13:47:33 +0200 Subject: [PATCH 07/10] Minor optimization to Stream.Read --- .../Extensions/StreamExtensions.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs index 331786d086d..74526cb1f0f 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/StreamExtensions.cs @@ -167,24 +167,24 @@ public static void Write(this Stream stream, ReadOnlySpan buffer) /// The value read from . /// Thrown if reaches the end. #if SPAN_RUNTIME_SUPPORT - // Avoid inlining as we're renting a stack buffer, which - // cause issues if this method was called inside a loop - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public static T Read(this Stream stream) where T : unmanaged { #if SPAN_RUNTIME_SUPPORT - Span span = stackalloc byte[Unsafe.SizeOf()]; + T result = default; + int length = Unsafe.SizeOf(); - if (stream.Read(span) != span.Length) + unsafe { - ThrowInvalidOperationExceptionForEndOfStream(); + if (stream.Read(new Span(&result, length)) != length) + { + ThrowInvalidOperationExceptionForEndOfStream(); + } } - ref byte r0 = ref MemoryMarshal.GetReference(span); - - return Unsafe.ReadUnaligned(ref r0); + return result; #else int length = Unsafe.SizeOf(); byte[] buffer = ArrayPool.Shared.Rent(length); From 7c3269a432a031e8e03423f0016752d579aaf8f7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 30 Oct 2020 13:33:21 +0100 Subject: [PATCH 08/10] Minor performance improvement --- .../Extensions/IBufferWriterExtensions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index e6ffb06f95a..ae87bea442f 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -8,6 +8,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Microsoft.Toolkit.HighPerformance.Buffers; using Microsoft.Toolkit.HighPerformance.Streams; using Microsoft.Toolkit.HighPerformance.Streams.Sources; @@ -28,6 +29,15 @@ public static class IBufferWriterExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Stream AsStream(this IBufferWriter writer) { + if (writer.GetType() == typeof(ArrayPoolBufferWriter)) + { + // If the input writer is of type ArrayPoolBufferWriter, we can use the type + // specific buffer writer owner to let the JIT elide callvirts when accessing it. + var internalWriter = Unsafe.As>(writer); + + return new IBufferWriterStream(new ArrayBufferWriterOwner(internalWriter)); + } + return new IBufferWriterStream(new IBufferWriterOwner(writer)); } From ba19b761e4bda40df50d137c3e60f47608bf074f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 30 Oct 2020 13:50:43 +0100 Subject: [PATCH 09/10] Added comments to unit tests --- .../Buffers/Test_ArrayPoolBufferWriter{T}.cs | 40 +++++++++++++------ .../Streams/Test_IBufferWriterStream.cs | 12 ++++++ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs index 08d30b2190f..042e10fe028 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Buffers/Test_ArrayPoolBufferWriter{T}.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -150,28 +149,43 @@ public void Test_ArrayPoolBufferWriterOfT_MultipleDispose() [TestMethod] public void Test_ArrayPoolBufferWriterOfT_AsStream() { - var writer = new ArrayPoolBufferWriter(); - - Span data = Guid.NewGuid().ToByteArray(); + const int GuidSize = 16; - data.CopyTo(writer.GetSpan(data.Length)); + var writer = new ArrayPoolBufferWriter(); + var guid = Guid.NewGuid(); - writer.Advance(data.Length); + // Here we first get a stream with the extension targeting ArrayPoolBufferWriter. + // This will wrap it into a custom internal stream type and produce a write-only + // stream that essentially mirrors the IBufferWriter functionality as a stream. + using (Stream writeStream = writer.AsStream()) + { + writeStream.Write(guid); + } - Assert.AreEqual(writer.WrittenCount, data.Length); + Assert.AreEqual(writer.WrittenCount, GuidSize); - Stream stream = ((IMemoryOwner)writer).AsStream(); + // Here we get a readable stream instead, and read from it to ensure + // the previous data was written correctly from the writeable stream. + using (Stream stream = writer.WrittenMemory.AsStream()) + { + Assert.AreEqual(stream.Length, GuidSize); - Assert.AreEqual(stream.Length, data.Length); + byte[] result = new byte[GuidSize]; - byte[] result = new byte[16]; + stream.Read(result, 0, result.Length); - stream.Read(result, 0, result.Length); + // Read the guid data and ensure it matches our initial guid + Assert.IsTrue(new Guid(result).Equals(guid)); + } - Assert.IsTrue(data.SequenceEqual(result)); + // Do a dummy write just to ensure the writer isn't disposed here. + // This is because we got a stream from a memory, not a memory owner. + writer.Write((byte)42); + writer.Advance(1); - stream.Dispose(); + writer.Dispose(); + // Now check that the writer is actually disposed instead Assert.ThrowsException(() => writer.Capacity); } } diff --git a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs index a5dabe764dc..bd3677c079c 100644 --- a/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs +++ b/UnitTests/UnitTests.HighPerformance.Shared/Streams/Test_IBufferWriterStream.cs @@ -22,6 +22,8 @@ public void Test_IBufferWriterStream_Lifecycle() { ArrayPoolBufferWriter writer = new ArrayPoolBufferWriter(); + // Get a stream from a buffer writer aand validate that it can only be written to. + // This is to mirror the same functionality as the IBufferWriter interface. Stream stream = ((IBufferWriter)writer).AsStream(); Assert.IsFalse(stream.CanRead); @@ -31,6 +33,7 @@ public void Test_IBufferWriterStream_Lifecycle() Assert.ThrowsException(() => stream.Length); Assert.ThrowsException(() => stream.Position); + // Dispose the stream and check that no operation is now allowed stream.Dispose(); Assert.IsFalse(stream.CanRead); @@ -49,11 +52,14 @@ public void Test_IBufferWriterStream_Write_Array() byte[] data = Test_MemoryStream.CreateRandomData(64); + // Write random data to the stream wrapping the buffer writer, and validate + // that the state of the writer is consistent, and the written content matches. stream.Write(data, 0, data.Length); Assert.AreEqual(writer.WrittenCount, data.Length); Assert.IsTrue(writer.WrittenSpan.SequenceEqual(data)); + // A few tests with invalid inputs (null buffers, invalid indices, etc.) Assert.ThrowsException(() => stream.Write(null, 0, 10)); Assert.ThrowsException(() => stream.Write(data, -1, 10)); Assert.ThrowsException(() => stream.Write(data, 200, 10)); @@ -74,6 +80,7 @@ public async Task Test_IBufferWriterStream_WriteAsync_Array() byte[] data = Test_MemoryStream.CreateRandomData(64); + // Same test as above, but using an asynchronous write instead await stream.WriteAsync(data, 0, data.Length); Assert.AreEqual(writer.WrittenCount, data.Length); @@ -102,11 +109,15 @@ public void Test_IBufferWriterStream_WriteByte() foreach (var item in data.Enumerate()) { + // Since we're enumerating, we can also double check the current written count + // at each iteration, to ensure the writes are done correctly every time. Assert.AreEqual(writer.WrittenCount, item.Index); + // Write a number of bytes one by one to test this API as well stream.WriteByte(item.Value); } + // Validate the final written length and actual data Assert.AreEqual(writer.WrittenCount, data.Length); Assert.IsTrue(data.SequenceEqual(writer.WrittenSpan)); @@ -140,6 +151,7 @@ public async Task Test_IBufferWriterStream_WriteAsync_Memory() Memory data = Test_MemoryStream.CreateRandomData(64); + // Same as the other asynchronous test above, but writing from a Memory await stream.WriteAsync(data); Assert.AreEqual(writer.WrittenCount, data.Length); From 4600e429ba9e2b3f3d5662d495843f1387c14533 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Nov 2020 12:30:29 +0100 Subject: [PATCH 10/10] Fixed nullability warning due to missing annotation --- .../Extensions/IBufferWriterExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index ae87bea442f..07a5bd6c2b8 100644 --- a/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/Microsoft.Toolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -33,7 +33,7 @@ public static Stream AsStream(this IBufferWriter writer) { // If the input writer is of type ArrayPoolBufferWriter, we can use the type // specific buffer writer owner to let the JIT elide callvirts when accessing it. - var internalWriter = Unsafe.As>(writer); + var internalWriter = Unsafe.As>(writer)!; return new IBufferWriterStream(new ArrayBufferWriterOwner(internalWriter)); }