From b8285b8356de091476c5e016d45aa3a5eb954c28 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Mon, 26 Mar 2018 14:36:08 -0700 Subject: [PATCH] Don't create the span on netstandard (#1721) - Directly pin the char[] - Changed Utf8BufferTextReader to use the Utf8Decoder - It copies whatever it can into the char buffer allocated in a stateful way (it's more efficient). - Added tests for unicode and ascii reading - Added a thread static cache --- .../Internal/Protocol/HandshakeProtocol.cs | 3 +- .../Internal/Protocol/JsonHubProtocol.cs | 17 ++- .../Internal/Protocol/Utf8BufferTextReader.cs | 73 ++++++++--- .../Properties/AssemblyInfo.cs | 3 +- .../Protocol/Utf8BufferTextReaderTests.cs | 119 ++++++++++++++++++ 5 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/Utf8BufferTextReaderTests.cs diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs index bc6168be43..8efbee6a26 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/HandshakeProtocol.cs @@ -58,7 +58,8 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol private static JsonTextReader CreateJsonTextReader(ReadOnlyMemory payload) { - var textReader = new Utf8BufferTextReader(payload); + var textReader = new Utf8BufferTextReader(); + textReader.SetBuffer(payload); var reader = new JsonTextReader(textReader); reader.ArrayPool = JsonArrayPool.Shared; diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs index ebd9f86d4d..80d7e56440 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/JsonHubProtocol.cs @@ -58,11 +58,19 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol { while (TextMessageParser.TryParseMessage(ref input, out var payload)) { - var textReader = new Utf8BufferTextReader(payload); - var message = ParseMessage(textReader, binder); - if (message != null) + var textReader = Utf8BufferTextReader.Get(payload); + + try { - messages.Add(message); + var message = ParseMessage(textReader, binder); + if (message != null) + { + messages.Add(message); + } + } + finally + { + Utf8BufferTextReader.Return(textReader); } } @@ -103,6 +111,7 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol using (var reader = new JsonTextReader(textReader)) { reader.ArrayPool = JsonArrayPool.Shared; + reader.CloseInput = false; JsonUtils.CheckRead(reader); diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/Utf8BufferTextReader.cs b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/Utf8BufferTextReader.cs index f09e475fb6..227f74fc98 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/Utf8BufferTextReader.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Internal/Protocol/Utf8BufferTextReader.cs @@ -11,10 +11,54 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol internal class Utf8BufferTextReader : TextReader { private ReadOnlyMemory _utf8Buffer; + private Decoder _decoder; - public Utf8BufferTextReader(ReadOnlyMemory utf8Buffer) + [ThreadStatic] + private static Utf8BufferTextReader _cachedInstance; + +#if DEBUG + private bool _inUse; +#endif + + public Utf8BufferTextReader() + { + _decoder = Encoding.UTF8.GetDecoder(); + } + + public static Utf8BufferTextReader Get(ReadOnlyMemory utf8Buffer) + { + var reader = _cachedInstance; + if (reader == null) + { + reader = new Utf8BufferTextReader(); + } + + // Taken off the the thread static + _cachedInstance = null; +#if DEBUG + if (reader._inUse) + { + throw new InvalidOperationException("The reader wasn't returned!"); + } + + reader._inUse = true; +#endif + reader.SetBuffer(utf8Buffer); + return reader; + } + + public static void Return(Utf8BufferTextReader reader) + { + _cachedInstance = reader; +#if DEBUG + reader._inUse = false; +#endif + } + + public void SetBuffer(ReadOnlyMemory utf8Buffer) { _utf8Buffer = utf8Buffer; + _decoder.Reset(); } public override int Read(char[] buffer, int index, int count) @@ -25,33 +69,24 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol } var source = _utf8Buffer.Span; - var destination = new Span(buffer, index, count); - var destinationBytesCount = Encoding.UTF8.GetByteCount(buffer, index, count); - - // We have then the destination - if (source.Length > destinationBytesCount) - { - source = source.Slice(0, destinationBytesCount); - - _utf8Buffer = _utf8Buffer.Slice(destinationBytesCount); - } - else - { - _utf8Buffer = ReadOnlyMemory.Empty; - } - + var bytesUsed = 0; + var charsUsed = 0; #if NETCOREAPP2_1 - return Encoding.UTF8.GetChars(source, destination); + var destination = new Span(buffer, index, count); + _decoder.Convert(source, destination, false, out bytesUsed, out charsUsed, out var completed); #else unsafe { - fixed (char* destinationChars = &MemoryMarshal.GetReference(destination)) + fixed (char* destinationChars = &buffer[index]) fixed (byte* sourceBytes = &MemoryMarshal.GetReference(source)) { - return Encoding.UTF8.GetChars(sourceBytes, source.Length, destinationChars, destination.Length); + _decoder.Convert(sourceBytes, source.Length, destinationChars, count, false, out bytesUsed, out charsUsed, out var completed); } } #endif + _utf8Buffer = _utf8Buffer.Slice(bytesUsed); + + return charsUsed; } } } diff --git a/src/Microsoft.AspNetCore.SignalR.Common/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.SignalR.Common/Properties/AssemblyInfo.cs index acee0ea403..92e52dceeb 100644 --- a/src/Microsoft.AspNetCore.SignalR.Common/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.SignalR.Common/Properties/AssemblyInfo.cs @@ -3,4 +3,5 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Tests.Utils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Tests.Utils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.SignalR.Common.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/Utf8BufferTextReaderTests.cs b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/Utf8BufferTextReaderTests.cs new file mode 100644 index 0000000000..08f921b104 --- /dev/null +++ b/test/Microsoft.AspNetCore.SignalR.Common.Tests/Internal/Protocol/Utf8BufferTextReaderTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.SignalR.Internal.Protocol; +using Xunit; + +namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol +{ + public class Utf8BufferTextReaderTests + { + [Fact] + public void ReadingWhenCharBufferBigEnough() + { + var buffer = Encoding.UTF8.GetBytes("Hello World"); + var reader = new Utf8BufferTextReader(); + reader.SetBuffer(buffer); + + var chars = new char[1024]; + int read = reader.Read(chars, 0, chars.Length); + + Assert.Equal("Hello World", new string(chars, 0, read)); + } + + [Fact] + public void ReadingUnicodeWhenCharBufferBigEnough() + { + var buffer = Encoding.UTF8.GetBytes("a\u00E4\u00E4\u00a9o"); + var reader = new Utf8BufferTextReader(); + reader.SetBuffer(buffer); + + var chars = new char[1024]; + int read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(5, read); + Assert.Equal("a\u00E4\u00E4\u00a9o", new string(chars, 0, read)); + + read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(0, read); + } + + [Fact] + public void ReadingWhenCharBufferBigEnoughAndNotStartingFromZero() + { + var buffer = Encoding.UTF8.GetBytes("Hello World"); + var reader = new Utf8BufferTextReader(); + reader.SetBuffer(buffer); + + var chars = new char[1024]; + int read = reader.Read(chars, 10, chars.Length - 10); + + Assert.Equal(11, read); + Assert.Equal("Hello World", new string(chars, 10, read)); + } + + [Fact] + public void ReadingWhenBufferTooSmall() + { + var buffer = Encoding.UTF8.GetBytes("Hello World"); + var reader = new Utf8BufferTextReader(); + reader.SetBuffer(buffer); + + var chars = new char[5]; + int read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(5, read); + Assert.Equal("Hello", new string(chars, 0, read)); + + read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(5, read); + Assert.Equal(" Worl", new string(chars, 0, read)); + + read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(1, read); + Assert.Equal("d", new string(chars, 0, read)); + + read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(0, read); + + read = reader.Read(chars, 0, 1); + + Assert.Equal(0, read); + } + + [Fact] + public void ReadingUnicodeWhenBufferTooSmall() + { + var buffer = Encoding.UTF8.GetBytes("\u00E4\u00E4\u00E5"); + var reader = new Utf8BufferTextReader(); + reader.SetBuffer(buffer); + + var chars = new char[2]; + int read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(2, read); + Assert.Equal("\u00E4\u00E4", new string(chars, 0, read)); + + read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(1, read); + Assert.Equal("\u00E5", new string(chars, 0, read)); + + read = reader.Read(chars, 0, chars.Length); + + Assert.Equal(0, read); + + read = reader.Read(chars, 0, 1); + + Assert.Equal(0, read); + } + } +} \ No newline at end of file