diff --git a/corefxlab.sln b/corefxlab.sln index b43c1dc38..966a749be 100644 --- a/corefxlab.sln +++ b/corefxlab.sln @@ -140,6 +140,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Pipelines.Extensi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Buffers.ReaderWriter", "src\System.Buffers.ReaderWriter\System.Buffers.ReaderWriter.csproj", "{C5F9D191-CA3B-4648-B8A9-62E33B4622EB}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Buffers.ReaderWriter.Tests", "tests\System.Buffers.ReaderWriter.Tests\System.Buffers.ReaderWriter.Tests.csproj", "{D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -906,6 +908,18 @@ Global {C5F9D191-CA3B-4648-B8A9-62E33B4622EB}.Release|x64.Build.0 = Release|Any CPU {C5F9D191-CA3B-4648-B8A9-62E33B4622EB}.Release|x86.ActiveCfg = Release|Any CPU {C5F9D191-CA3B-4648-B8A9-62E33B4622EB}.Release|x86.Build.0 = Release|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Debug|x64.Build.0 = Debug|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Debug|x86.Build.0 = Debug|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Release|Any CPU.Build.0 = Release|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Release|x64.ActiveCfg = Release|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Release|x64.Build.0 = Release|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Release|x86.ActiveCfg = Release|Any CPU + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -974,6 +988,7 @@ Global {916370AB-B0D3-4136-850B-AA12FAB23ECD} = {4B000021-5278-4F2A-B734-DE49F55D4024} {64C08774-982C-4141-8F8D-2884B6FA0E4B} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} {C5F9D191-CA3B-4648-B8A9-62E33B4622EB} = {4B000021-5278-4F2A-B734-DE49F55D4024} + {D9FFEC52-B701-4DB5-969C-BAC4F8EB220C} = {3079E458-D0E6-4F99-8CAB-80011D35C7DA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9DD4022C-A010-4A9B-BCC5-171566D4CB17} diff --git a/src/System.Text.Primitives/System/Buffers/IWritable.cs b/src/System.Buffers.Primitives/System/Buffers/IWritable.cs similarity index 100% rename from src/System.Text.Primitives/System/Buffers/IWritable.cs rename to src/System.Buffers.Primitives/System/Buffers/IWritable.cs diff --git a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter.cs b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter.cs index 08b7be670..ffbe09f43 100644 --- a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter.cs +++ b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter.cs @@ -2,8 +2,8 @@ // 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.Buffers.Text; +using System.Runtime.CompilerServices; namespace System.Buffers.Writer { @@ -17,8 +17,10 @@ namespace System.Buffers.Writer static byte[] s_defaultNewline = new byte[] { (byte)'\n' }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static BufferWriter Create(Span buffer) => new BufferWriter(buffer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static BufferWriter Create(TOutput output) where TOutput : IBufferWriter => new BufferWriter(output); diff --git a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter_sequence.cs b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter_sequence.cs deleted file mode 100644 index 97678ad14..000000000 --- a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/BufferWriter_sequence.cs +++ /dev/null @@ -1,231 +0,0 @@ -// 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.Buffers.Text; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace System.Buffers.Writer -{ - public ref struct BufferWriter where TOutput : IBufferWriter - { - TOutput _output; - Span _buffer; - int _written; - - public BufferWriter(TOutput output) - { - _output = output; - _buffer = _output.GetSpan(); - _written = 0; - } - - public void Flush() - { - _output.Advance(_written); - _buffer = _output.GetSpan(); - _written = 0; - } - - public void WriteBytes(byte[] bytes) - { - var free = Free; - if (bytes.Length > 0 && free.Length >= bytes.Length) - { - ref byte pSource = ref bytes[0]; - ref byte pDest = ref MemoryMarshal.GetReference(free); - - Unsafe.CopyBlockUnaligned(ref pDest, ref pSource, (uint)bytes.Length); - - Advance(bytes.Length); - } - else - { - WriteBytesChunked(bytes, 0, bytes.Length); - } - } - - public void WriteBytes(byte[] bytes, int index, int length) - { - var free = Free; - - // If offset or length is negative the cast to uint will make them larger than int.MaxValue - // so each test both tests for negative values and greater than values. This pattern wil also - // elide the second bounds check that would occur at source[offset]; as is pre-checked - // https://github.com/dotnet/coreclr/pull/9773 - if ((uint)index > (uint)bytes.Length || (uint)length > (uint)(bytes.Length - index)) - { - // Only need to pass in array length and offset for ThrowHelper to determine which test failed - ThrowArgumentOutOfRangeException(bytes.Length, index); - } - - if (length > 0 && free.Length >= length) - { - ref byte pSource = ref bytes[index]; - ref byte pDest = ref MemoryMarshal.GetReference(free); - - Unsafe.CopyBlockUnaligned(ref pDest, ref pSource, (uint)length); - - Advance(length); - } - else - { - WriteBytesChunked(bytes, index, length); - } - } - - public void WriteBytes(ReadOnlySpan bytes) - { - var free = Free; - if (bytes.TryCopyTo(free)) - { - Advance(bytes.Length); - return; - } - WriteBytesChunked(bytes); - } - - public void WriteBytes(ReadOnlyMemory bytes) - => WriteBytes(bytes.Span); - - public void WriteBytes(T value, StandardFormat format = default) where T : IWritable - { - var free = Free; - int written; - while (!value.TryWrite(free, out written, format)) - { - free = Enlarge(); - } - Advance(written); - } - - public void WriteBytes(T value, TransformationFormat format) where T : IWritable - { - var free = Free; - int written; - while (true) - { - while (!value.TryWrite(free, out written, format.Format)) - { - free = Enlarge(); - } - if (format.TryTransform(free, ref written)) break; - free = Enlarge(); - } - Advance(written); - } - - public void Write(int value, StandardFormat format = default) - { - var free = Free; - int written; - while (!Utf8Formatter.TryFormat(value, free, out written, format)) - { - free = Enlarge(); - } - Advance(written); - } - - public void Write(string value) - { - var utf16Bytes = value.AsSpan().AsBytes(); - while (true) - { - var free = Free; - // TODO: shouldn't it be easier if Free never returned an empty span? - if (free.Length == 0) - { - free = Enlarge(); - } - var status = Encodings.Utf16.ToUtf8(utf16Bytes, free, out var consumed, out int written); - switch (status) - { - case OperationStatus.Done: - Advance(written); - return; - case OperationStatus.DestinationTooSmall: - Advance(written); - utf16Bytes = utf16Bytes.Slice(consumed); - break; - default: - throw new ArgumentOutOfRangeException(nameof(value)); - } - } - } - - private void WriteBytesChunked(ReadOnlySpan bytes) - { - var length = bytes.Length; - var index = 0; - while (length > 0) - { - var free = Free; - if (free.Length == 0) - { - free = Enlarge(); - } - - var chunkLength = Math.Min(length, free.Length); - - var chunk = bytes.Slice(index, chunkLength); - chunk.CopyTo(free); - Advance(chunkLength); - - length -= chunkLength; - index += chunkLength; - } - } - private void WriteBytesChunked(byte[] bytes, int index, int length) - { - while (length > 0) - { - var free = Free; - if (free.Length == 0) - { - free = Enlarge(); - } - - var chunkLength = Math.Min(length, free.Length); - - ref byte pSource = ref bytes[index]; - ref byte pDest = ref MemoryMarshal.GetReference(free); - - Unsafe.CopyBlockUnaligned(ref pDest, ref pSource, (uint)chunkLength); - - Advance(chunkLength); - - length -= chunkLength; - index += chunkLength; - } - } - - private void ThrowArgumentOutOfRangeException(int length, int index) - { - if ((uint)index > (uint)length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - throw new ArgumentOutOfRangeException(nameof(length)); - } - - private Span Free => _buffer.Slice(_written); - - private Span Enlarge(int desiredBufferSize = 0) - { - var before = _buffer.Length - _written; - Flush(); // This sets _written to 0 - Debug.Assert(_written == 0); - if (_buffer.Length > before) return _buffer; - - _output.GetMemory(desiredBufferSize); - _buffer = _output.GetSpan(); - Debug.Assert(_written == 0); // ensure still 0 - return _buffer; - } - - private void Advance(int count) - => _written += count; - } -} diff --git a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputExtensions.cs b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputExtensions.cs deleted file mode 100644 index f0e898453..000000000 --- a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.Buffers.Writer -{ - public static class OutputExtensions - { - public static void Write(this IBufferWriter bufferWriter, ReadOnlySpan source) - { - var buffer = bufferWriter.GetMemory(); - - // Fast path, try copying to the available memory directly - if (source.Length <= buffer.Length) - { - source.CopyTo(buffer.Span); - bufferWriter.Advance(source.Length); - return; - } - - var remaining = source.Length; - var offset = 0; - - while (remaining > 0) - { - var writable = Math.Min(remaining, buffer.Length); - - buffer = bufferWriter.GetMemory(writable); - - if (writable == 0) - { - continue; - } - - source.Slice(offset, writable).CopyTo(buffer.Span); - - remaining -= writable; - offset += writable; - - bufferWriter.Advance(writable); - } - } - } -} diff --git a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter.cs b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter.cs index f25bdfae7..37521191e 100644 --- a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter.cs +++ b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter.cs @@ -6,31 +6,23 @@ using System.Runtime.CompilerServices; namespace System.Buffers.Writer { - public static class OutputWriter - { - public static OutputWriter Create(T output) where T : IBufferWriter - { - return new OutputWriter(output); - } - } - - public ref struct OutputWriter where T : IBufferWriter + public ref partial struct BufferWriter where T : IBufferWriter { private T _output; private Span _span; private int _buffered; - public OutputWriter(T output) + public BufferWriter(T output) { _buffered = 0; _output = output; _span = output.GetSpan(); } - public Span Span => _span; + public Span Buffer => _span; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Commit() + public void Flush() { var buffered = _buffered; if (buffered > 0) @@ -47,20 +39,6 @@ namespace System.Buffers.Writer _span = _span.Slice(count); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Write(ReadOnlySpan source) - { - if (_span.Length >= source.Length) - { - source.CopyTo(_span); - Advance(source.Length); - } - else - { - WriteMultiBuffer(source); - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Ensure(int count = 1) { @@ -73,29 +51,19 @@ namespace System.Buffers.Writer [MethodImpl(MethodImplOptions.NoInlining)] private void EnsureMore(int count = 0) { - if (_buffered > 0) + var buffered = _buffered; + if (buffered > 0) { - Commit(); + _buffered = 0; + _output.Advance(buffered); } - - _output.GetMemory(count); - _span = _output.GetSpan(); + _span = _output.GetSpan(count); } - private void WriteMultiBuffer(ReadOnlySpan source) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Enlarge() { - while (source.Length > 0) - { - if (_span.Length == 0) - { - EnsureMore(); - } - - var writable = Math.Min(source.Length, _span.Length); - source.Slice(0, writable).CopyTo(_span); - source = source.Slice(writable); - Advance(writable); - } + EnsureMore(_span.Length + 1); } } } diff --git a/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter_overloads.cs b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter_overloads.cs new file mode 100644 index 000000000..3adf48254 --- /dev/null +++ b/src/System.Buffers.ReaderWriter/System/Buffers/Writer/OutputWriter_overloads.cs @@ -0,0 +1,114 @@ +// 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.Buffers.Text; +using System.Runtime.CompilerServices; + +namespace System.Buffers.Writer +{ + public ref partial struct BufferWriter where T : IBufferWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(int value, StandardFormat format = default) + { + int written; + while (!Utf8Formatter.TryFormat(value, Buffer, out written, format)) + { + Enlarge(); + } + Advance(written); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ulong value, StandardFormat format = default) + { + int written; + while (!Utf8Formatter.TryFormat(value, Buffer, out written, format)) + { + Enlarge(); + } + Advance(written); + } + + public void Write(string value) + { + ReadOnlySpan utf16Bytes = value.AsSpan().AsBytes(); + int totalConsumed = 0; + while (true) + { + var status = Encodings.Utf16.ToUtf8(utf16Bytes.Slice(totalConsumed), Buffer, out int consumed, out int written); + switch (status) + { + case OperationStatus.Done: + Advance(written); + return; + case OperationStatus.DestinationTooSmall: + Advance(written); + Enlarge(); + break; + case OperationStatus.NeedMoreData: + case OperationStatus.InvalidData: + throw new ArgumentOutOfRangeException(nameof(value)); + } + totalConsumed += consumed; + } + } + + public void Write(TWritable value, TransformationFormat format) where TWritable : IWritable + { + int written; + while (true) + { + while (!value.TryWrite(Buffer, out written, format.Format)) + { + Enlarge(); + } + if (format.TryTransform(Buffer, ref written)) break; + Enlarge(); + } + Advance(written); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(TWritable value, StandardFormat format) where TWritable : IWritable + { + int written; + while (!value.TryWrite(Buffer, out written, format)) + { + Enlarge(); + } + Advance(written); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan source) + { + if (_span.Length >= source.Length) + { + source.CopyTo(_span); + Advance(source.Length); + } + else + { + WriteMultiBuffer(source); + } + } + + private void WriteMultiBuffer(ReadOnlySpan source) + { + while (source.Length > 0) + { + if (_span.Length == 0) + { + EnsureMore(); + } + + var writable = Math.Min(source.Length, _span.Length); + source.Slice(0, writable).CopyTo(_span); + source = source.Slice(writable); + Advance(writable); + } + } + } +} diff --git a/tests/Benchmarks/Benchmarks.csproj b/tests/Benchmarks/Benchmarks.csproj index 4434eafed..fb91b8f04 100644 --- a/tests/Benchmarks/Benchmarks.csproj +++ b/tests/Benchmarks/Benchmarks.csproj @@ -30,6 +30,7 @@ + diff --git a/tests/Benchmarks/BufferWriterBench.cs b/tests/Benchmarks/BufferWriterBench.cs new file mode 100644 index 000000000..fa9f1896f --- /dev/null +++ b/tests/Benchmarks/BufferWriterBench.cs @@ -0,0 +1,527 @@ +// 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 Microsoft.Xunit.Performance; +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Buffers.Writer; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Utf8; +using System.Threading; + +public class BufferWriterBench +{ + private static AsciiString s_crlf = "\r\n"; + private static AsciiString s_eoh = "\r\n\r\n"; // End Of Headers + private static AsciiString s_http11OK = "HTTP/1.1 200 OK\r\n"; + private static AsciiString s_headerServer = "Server: Custom"; + private static AsciiString s_headerContentLength = "Content-Length: "; + private static AsciiString s_headerContentLengthZero = "Content-Length: 0\r\n"; + private static AsciiString s_headerContentTypeText = "Content-Type: text/plain\r\n"; + private static AsciiString s_plainTextBody = "Hello, World!"; + + private static Utf8String s_crlfU8 = (Utf8String)"\r\n"; + private static Utf8String s_eohU8 = (Utf8String)"\r\n\r\n"; // End Of Headers + private static Utf8String s_http11OKU8 = (Utf8String)"HTTP/1.1 200 OK\r\n"; + private static Utf8String s_headerServerU8 = (Utf8String)"Server: Custom"; + private static Utf8String s_headerContentLengthU8 = (Utf8String)"Content-Length: "; + private static Utf8String s_headerContentLengthZeroU8 = (Utf8String)"Content-Length: 0\r\n"; + private static Utf8String s_headerContentTypeTextU8 = (Utf8String)"Content-Type: text/plain\r\n"; + private static Utf8String s_plainTextBodyU8 = (Utf8String)"Hello, World!"; + + private static Sink s_sink = new Sink(4096); + + const int InnerIterations = 1000000; + + [Benchmark(InnerIterationCount = InnerIterations)] + static void PlatfromBenchmarkPlaintext() + { + foreach (var iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < InnerIterations; i++) + { + s_sink.Reset(); + var writer = new PlatfromBenchmark.BufferWriter(s_sink); + + // HTTP 1.1 OK + writer.Write(s_http11OK); + + // Server headers + writer.Write(s_headerServer); + + // Date header + writer.Write(DateHeader.HeaderBytes); + + // Content-Type header + writer.Write(s_headerContentTypeText); + + // Content-Length header + writer.Write(s_headerContentLength); + writer.Write((ulong)s_plainTextBody.Length); + + // End of headers + writer.Write(s_eoh); + + // Body + writer.Write(s_plainTextBody); + writer.Commit(); + } + } + } + } + + [Benchmark(InnerIterationCount = InnerIterations)] + static void BufferWriterPlaintext() + { + foreach (var iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < InnerIterations; i++) + { + s_sink.Reset(); + var writer = BufferWriter.Create(s_sink); + + // HTTP 1.1 OK + writer.Write(s_http11OK); + + // Server headers + writer.Write(s_headerServer); + + // Date header + writer.Write(DateHeader.HeaderBytes); + + // Content-Type header + writer.Write(s_headerContentTypeText); + + // Content-Length header + writer.Write(s_headerContentLength); + writer.Write((ulong)s_plainTextBody.Length); + + // End of headers + writer.Write(s_eoh); + + // Body + writer.Write(s_plainTextBody); + writer.Flush(); + } + } + } + } + + [Benchmark(InnerIterationCount = InnerIterations)] + static void BufferWriterPlaintextUtf8() + { + foreach (var iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < InnerIterations; i++) + { + s_sink.Reset(); + var writer = BufferWriter.Create(s_sink); + + // HTTP 1.1 OK + writer.Write(s_http11OKU8); + + // Server headers + writer.Write(s_headerServerU8); + + // Date header + writer.Write(DateHeader.HeaderBytes); + + // Content-Type header + writer.Write(s_headerContentTypeTextU8); + + // Content-Length header + writer.Write(s_headerContentLengthU8); + writer.Write((ulong)s_plainTextBody.Length); + + // End of headers + writer.Write(s_eohU8); + + // Body + writer.Write(s_plainTextBodyU8); + writer.Flush(); + } + } + } + } + + [Benchmark(InnerIterationCount = InnerIterations)] + static void BufferWriterCopyPlaintext() + { + foreach (var iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < InnerIterations; i++) + { + s_sink.Reset(); + var writer = new SystemBuffers.BufferWriter(s_sink); + + // HTTP 1.1 OK + writer.Write(s_http11OK); + + // Server headers + writer.Write(s_headerServer); + + // Date header + writer.Write(DateHeader.HeaderBytes); + + // Content-Type header + writer.Write(s_headerContentTypeText); + + // Content-Length header + writer.Write(s_headerContentLength); + writer.Write((ulong)s_plainTextBody.Length); + + // End of headers + writer.Write(s_eoh); + + // Body + writer.Write(s_plainTextBody); + writer.Flush(); + } + } + } + } +} + +class Sink : IBufferWriter +{ + byte[] _buffer; + int _written; + + public Sink(int size) + { + _buffer = new byte[4096]; + _written = 0; + } + public void Reset() => _written = 0; + + public void Advance(int count) + { + _written += count; + if (_written > _buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); + } + + public Memory GetMemory(int sizeHint = 0) + => _buffer.AsMemory(_written, _buffer.Length - _written); + + public Span GetSpan(int sizeHint = 0) + => _buffer.AsSpan(_written, _buffer.Length - _written); +} + +readonly struct AsciiString : IEquatable +{ + private readonly byte[] _data; + + public AsciiString(string s) => _data = Encoding.ASCII.GetBytes(s); + + public int Length => _data.Length; + + public ReadOnlySpan AsSpan() => _data; + + public static implicit operator ReadOnlySpan(AsciiString str) => str._data; + public static implicit operator byte[] (AsciiString str) => str._data; + + public static implicit operator AsciiString(string str) => new AsciiString(str); + + public override string ToString() => Encoding.ASCII.GetString(_data); + + public static explicit operator string(AsciiString str) => str.ToString(); + + public bool Equals(AsciiString other) => ReferenceEquals(_data, other._data) || SequenceEqual(_data, other._data); + private bool SequenceEqual(byte[] data1, byte[] data2) => new Span(data1).SequenceEqual(data2); + + public static bool operator ==(AsciiString a, AsciiString b) => a.Equals(b); + public static bool operator !=(AsciiString a, AsciiString b) => !a.Equals(b); + public override bool Equals(object other) => (other is AsciiString) && Equals((AsciiString)other); + + public override int GetHashCode() + { + // Copied from x64 version of string.GetLegacyNonRandomizedHashCode() + // https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/String.Comparison.cs + var data = _data; + int hash1 = 5381; + int hash2 = hash1; + foreach (int b in data) + { + hash1 = ((hash1 << 5) + hash1) ^ b; + } + return hash1 + (hash2 * 1566083941); + } +} + +static class DateHeader +{ + const int prefixLength = 8; // "\r\nDate: ".Length + const int dateTimeRLength = 29; // Wed, 14 Mar 2018 14:20:00 GMT + const int suffixLength = 2; // crlf + const int suffixIndex = dateTimeRLength + prefixLength; + + private static byte[] s_headerBytesMaster = new byte[prefixLength + dateTimeRLength + suffixLength]; + private static byte[] s_headerBytesScratch = new byte[prefixLength + dateTimeRLength + suffixLength]; + + static DateHeader() + { + var utf8 = Encoding.ASCII.GetBytes("\r\nDate: ").AsSpan(); + utf8.CopyTo(s_headerBytesMaster); + utf8.CopyTo(s_headerBytesScratch); + s_headerBytesMaster[suffixIndex] = (byte)'\r'; + s_headerBytesMaster[suffixIndex + 1] = (byte)'\n'; + s_headerBytesScratch[suffixIndex] = (byte)'\r'; + s_headerBytesScratch[suffixIndex + 1] = (byte)'\n'; + SetDateValues(DateTimeOffset.UtcNow); + } + + public static ReadOnlySpan HeaderBytes => s_headerBytesMaster; + + public static void SetDateValues(DateTimeOffset value) + { + lock (s_headerBytesScratch) + { + if (!Utf8Formatter.TryFormat(value, s_headerBytesScratch.AsSpan().Slice(prefixLength), out int written, 'R')) + { + throw new Exception("date time format failed"); + } + Debug.Assert(written == dateTimeRLength); + var temp = s_headerBytesMaster; + s_headerBytesMaster = s_headerBytesScratch; + s_headerBytesScratch = temp; + } + } +} + +// copy from https://github.com/aspnet/benchmarks/tree/dev/src/PlatformBenchmarks +namespace PlatfromBenchmark +{ + internal ref struct BufferWriter where T : IBufferWriter + { + private T _output; + private Span _span; + private int _buffered; + + public BufferWriter(T output) + { + _buffered = 0; + _output = output; + _span = output.GetSpan(); + } + + public Span Span => _span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Commit() + { + var buffered = _buffered; + if (buffered > 0) + { + _buffered = 0; + _output.Advance(buffered); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + _buffered += count; + _span = _span.Slice(count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan source) + { + if (_span.Length >= source.Length) + { + source.CopyTo(_span); + Advance(source.Length); + } + else + { + WriteMultiBuffer(source); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Ensure(int count = 1) + { + if (_span.Length < count) + { + EnsureMore(count); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureMore(int count = 0) + { + if (_buffered > 0) + { + Commit(); + } + + _output.GetMemory(count); + _span = _output.GetSpan(); + } + + private void WriteMultiBuffer(ReadOnlySpan source) + { + while (source.Length > 0) + { + if (_span.Length == 0) + { + EnsureMore(); + } + + var writable = Math.Min(source.Length, _span.Length); + source.Slice(0, writable).CopyTo(_span); + source = source.Slice(writable); + Advance(writable); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ulong number) + { + // Try to format directly + if (Utf8Formatter.TryFormat(number, Span, out int bytesWritten)) + { + Advance(bytesWritten); + } + else + { + // Ask for at least 20 bytes + Ensure(20); + + Debug.Assert(Span.Length >= 20, "Buffer is < 20 bytes"); + + // Try again + if (Utf8Formatter.TryFormat(number, Span, out bytesWritten)) + { + Advance(bytesWritten); + } + } + } + } +} + +// copy from System.Buffers.ReaderWriter to isolate cross-dll calls. +namespace SystemBuffers +{ + public ref partial struct BufferWriter where T : IBufferWriter + { + private T _output; + private Span _span; + private int _buffered; + + public BufferWriter(T output) + { + _buffered = 0; + _output = output; + _span = output.GetSpan(); + } + + public Span Buffer => _span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Flush() + { + var buffered = _buffered; + if (buffered > 0) + { + _buffered = 0; + _output.Advance(buffered); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + _buffered += count; + _span = _span.Slice(count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Ensure(int count = 1) + { + if (_span.Length < count) + { + EnsureMore(count); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureMore(int count = 0) + { + var buffered = _buffered; + if (buffered > 0) + { + _buffered = 0; + _output.Advance(buffered); + } + _span = _output.GetSpan(count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Enlarge() + { + EnsureMore(_span.Length + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan source) + { + if (_span.Length >= source.Length) + { + source.CopyTo(_span); + Advance(source.Length); + } + else + { + WriteMultiBuffer(source); + } + } + + private void WriteMultiBuffer(ReadOnlySpan source) + { + while (source.Length > 0) + { + if (_span.Length == 0) + { + EnsureMore(); + } + + var writable = Math.Min(source.Length, _span.Length); + source.Slice(0, writable).CopyTo(_span); + source = source.Slice(writable); + Advance(writable); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ulong value, StandardFormat format = default) + { + int written; + if (Utf8Formatter.TryFormat(value, Buffer, out written, format)) + { + Advance(written); + } + else + { + Enlarge(); + while (!Utf8Formatter.TryFormat(value, Buffer, out written, format)) + { + Enlarge(); + } + Advance(written); + } + } + } +} diff --git a/tests/System.Buffers.Experimental.Tests/BufferWriterTests_sequence.cs b/tests/System.Buffers.Experimental.Tests/BufferWriterTests_sequence.cs index 3a787e005..9b6b4ccba 100644 --- a/tests/System.Buffers.Experimental.Tests/BufferWriterTests_sequence.cs +++ b/tests/System.Buffers.Experimental.Tests/BufferWriterTests_sequence.cs @@ -19,8 +19,8 @@ namespace System.Buffers.Tests { IBufferWriter bufferWriter = new TestBufferWriter(); var writer = BufferWriter.Create(bufferWriter); - writer.WriteBytes(Encoding.UTF8.GetBytes("Hello")); - writer.WriteBytes(Encoding.UTF8.GetBytes(" World!")); + writer.Write(Encoding.UTF8.GetBytes("Hello")); + writer.Write(Encoding.UTF8.GetBytes(" World!")); writer.Flush(); Assert.Equal("Hello World!", bufferWriter.ToString()); } @@ -35,8 +35,8 @@ namespace System.Buffers.Tests ulonger.Lower = ulong.MaxValue; ulonger.Upper = 1; - writer.WriteBytes(ulonger, s_base64); - writer.WriteBytes(ulonger, 't'); + writer.Write(ulonger, s_base64); + writer.Write(ulonger, 't'); writer.Write(123); writer.Write("This is just a longish string"); writer.Flush(); diff --git a/tests/System.Buffers.ReaderWriter.Tests/BasicUnitTests.cs b/tests/System.Buffers.ReaderWriter.Tests/BasicUnitTests.cs new file mode 100644 index 000000000..ca9b47aa3 --- /dev/null +++ b/tests/System.Buffers.ReaderWriter.Tests/BasicUnitTests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Buffers.Text; +using System.Buffers.Writer; +using System.Diagnostics; +using System.Text; +using System.Text.Utf8; +using System.Threading; +using Xunit; + +namespace System.Buffers.Tests +{ + public class BasicUnitTests + { + private static Utf8String _crlf = (Utf8String)"\r\n"; + private static Utf8String _eoh = (Utf8String)"\r\n\r\n"; // End Of Headers + private static Utf8String _http11OK = (Utf8String)"HTTP/1.1 200 OK\r\n"; + private static Utf8String _headerServer = (Utf8String)"Server: Custom"; + private static Utf8String _headerContentLength = (Utf8String)"Content-Length: "; + private static Utf8String _headerContentLengthZero = (Utf8String)"Content-Length: 0\r\n"; + private static Utf8String _headerContentTypeText = (Utf8String)"Content-Type: text/plain\r\n"; + + private static Utf8String _plainTextBody = (Utf8String)"Hello, World!"; + + static Sink _sink = new Sink(4096); + static string s_response = "HTTP/1.1 200 OK\r\nServer: Custom\r\nDate: Fri, 16 Mar 2018 10:22:15 GMT\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!"; + + [Fact] + public void WritePlainText() + { + DateHeader.SetDateValues(new DateTimeOffset(2018, 3, 16, 10, 22, 15, 10, TimeSpan.FromMilliseconds(0))); + + _sink.Reset(); + var writer = BufferWriter.Create(_sink); + + // HTTP 1.1 OK + writer.Write(_http11OK); + + // Server headers + writer.Write(_headerServer); + + // Date header + writer.Write(DateHeader.HeaderBytes); + + // Content-Type header + writer.Write(_headerContentTypeText); + + // Content-Length header + writer.Write(_headerContentLength); + writer.Write((ulong)_plainTextBody.Bytes.Length); + + // End of headers + writer.Write(_eoh); + + // Body + writer.Write(_plainTextBody); + writer.Flush(); + + var result = _sink.ToString(); + Assert.Equal(s_response, _sink.ToString()); + } + } + + class Sink : IBufferWriter + { + byte[] _buffer; + int _written; + + public Sink(int size) + { + _buffer = new byte[4096]; + _written = 0; + } + public void Reset() => _written = 0; + + public void Advance(int count) + { + _written += count; + if (_written > _buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); + } + + public Memory GetMemory(int sizeHint = 0) + => _buffer.AsMemory(_written, _buffer.Length - _written); + + public Span GetSpan(int sizeHint = 0) + => _buffer.AsSpan(_written, _buffer.Length - _written); + + public override string ToString() + { + return Encoding.UTF8.GetString(_buffer, 0, _written); + } + } + + static class DateHeader + { + const int prefixLength = 8; // "\r\nDate: ".Length + const int dateTimeRLength = 29; // Wed, 14 Mar 2018 14:20:00 GMT + const int suffixLength = 2; // crlf + const int suffixIndex = dateTimeRLength + prefixLength; + + private static byte[] s_headerBytesMaster = new byte[prefixLength + dateTimeRLength + suffixLength]; + private static byte[] s_headerBytesScratch = new byte[prefixLength + dateTimeRLength + suffixLength]; + + static DateHeader() + { + var utf8 = Encoding.ASCII.GetBytes("\r\nDate: ").AsSpan(); + utf8.CopyTo(s_headerBytesMaster); + utf8.CopyTo(s_headerBytesScratch); + s_headerBytesMaster[suffixIndex] = (byte)'\r'; + s_headerBytesMaster[suffixIndex + 1] = (byte)'\n'; + s_headerBytesScratch[suffixIndex] = (byte)'\r'; + s_headerBytesScratch[suffixIndex + 1] = (byte)'\n'; + SetDateValues(DateTimeOffset.UtcNow); + } + + public static ReadOnlySpan HeaderBytes => s_headerBytesMaster; + + public static void SetDateValues(DateTimeOffset value) + { + lock (s_headerBytesScratch) + { + if (!Utf8Formatter.TryFormat(value, s_headerBytesScratch.AsSpan().Slice(prefixLength), out int written, 'R')) + { + throw new Exception("date time format failed"); + } + Debug.Assert(written == dateTimeRLength); + var temp = s_headerBytesMaster; + s_headerBytesMaster = s_headerBytesScratch; + s_headerBytesScratch = temp; + } + } + } +} diff --git a/tests/System.Buffers.ReaderWriter.Tests/System.Buffers.ReaderWriter.Tests.csproj b/tests/System.Buffers.ReaderWriter.Tests/System.Buffers.ReaderWriter.Tests.csproj new file mode 100644 index 000000000..6959b235c --- /dev/null +++ b/tests/System.Buffers.ReaderWriter.Tests/System.Buffers.ReaderWriter.Tests.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.1 + True + ../../tools/test_key.snk + true + true + + Microsoft Corporation, All rights reserved + + + + + + False + + + + + + + + + + + + + + + + diff --git a/tests/System.IO.Pipelines.Performance.Tests/PipeThroughput.cs b/tests/System.IO.Pipelines.Performance.Tests/PipeThroughput.cs index b4e3cf6f8..c48a6b3c5 100644 --- a/tests/System.IO.Pipelines.Performance.Tests/PipeThroughput.cs +++ b/tests/System.IO.Pipelines.Performance.Tests/PipeThroughput.cs @@ -143,7 +143,7 @@ namespace System.IO.Pipelines.Performance.Tests for (int i = 0; i < InnerLoopCount; i++) { var writableBuffer = _pipe.Writer; - var writer = OutputWriter.Create(writableBuffer); + var writer = BufferWriter.Create(writableBuffer); foreach (var write in _plaintextWrites) {