From e86713e94970dcc2e903f9f3dc5d284e39ad17fe Mon Sep 17 00:00:00 2001 From: JKorf Date: Thu, 21 Mar 2024 16:46:17 +0100 Subject: [PATCH] Performance improvements --- .../TestImplementations/TestBaseClient.cs | 2 +- .../Authentication/ApiCredentials.cs | 2 +- .../Authentication/AuthenticationProvider.cs | 4 +-- CryptoExchange.Net/Clients/RestApiClient.cs | 4 +-- .../JsonNet/JsonNetMessageAccessor.cs | 34 ++++++++++++++----- .../SystemTextJson/EnumConverter.cs | 9 +++-- .../SystemTextJsonMessageAccessor.cs | 21 +++++++++--- CryptoExchange.Net/ExtensionMethods.cs | 18 ++++++++++ .../Interfaces/IMessageAccessor.cs | 3 +- .../Interfaces/IMessageProcessor.cs | 2 +- .../Sockets/SocketConnection.cs | 5 +-- 11 files changed, 80 insertions(+), 24 deletions(-) diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 6752845..c696169 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -46,7 +46,7 @@ namespace CryptoExchange.Net.UnitTests { var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); var accessor = CreateAccessor(); - var valid = accessor.Read(stream, true); + var valid = accessor.Read(stream, true).Result; if (!valid) return new CallResult(new ServerError(data)); diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index a0cae49..b942853 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -95,7 +95,7 @@ namespace CryptoExchange.Net.Authentication public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) { var accessor = new SystemTextJsonStreamMessageAccessor(); - if (!accessor.Read(inputStream, false)) + if (!accessor.Read(inputStream, false).Result) throw new ArgumentException("Input stream not valid json data"); var key = accessor.GetValue(MessagePath.Get().Property(identifierKey ?? "apiKey")); diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 5221546..6d2f5e2 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -248,7 +248,7 @@ namespace CryptoExchange.Net.Authentication } /// - /// HMACSHA512 sign the data and return the hash + /// HMACSHA256 sign the data and return the hash /// /// Data to sign /// String type @@ -270,7 +270,7 @@ namespace CryptoExchange.Net.Authentication } /// - /// HMACSHA512 sign the data and return the hash + /// HMACSHA384 sign the data and return the hash /// /// Data to sign /// String type diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 601c4ae..59251b6 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -323,7 +323,7 @@ namespace CryptoExchange.Net.Clients if (!response.IsSuccessStatusCode) { // Error response - accessor.Read(responseStream, true); + await accessor.Read(responseStream, true).ConfigureAwait(false); Error error; if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) @@ -341,7 +341,7 @@ namespace CryptoExchange.Net.Clients // Success status code and expected empty response, assume it's correct return new WebCallResult(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null); - var valid = accessor.Read(responseStream, outputOriginalData); + var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); if (!valid) { // Invalid json diff --git a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs index 9aa1e80..240763a 100644 --- a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; +using System.Threading.Tasks; namespace CryptoExchange.Net.Converters.JsonNet { @@ -222,26 +224,32 @@ namespace CryptoExchange.Net.Converters.JsonNet public override bool OriginalDataAvailable => _stream?.CanSeek == true; /// - public bool Read(Stream stream, bool bufferStream) + public async Task Read(Stream stream, bool bufferStream) { if (bufferStream && stream is not MemoryStream) { + // We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream _stream = new MemoryStream(); stream.CopyTo(_stream); _stream.Position = 0; } - else + else if (bufferStream) { + // We need to buffer the stream, and the current stream is seekable, store as is _stream = stream; } + else + { + // We don't need to buffer the stream, so don't bother keeping the reference + } - var length = _stream.CanSeek ? _stream.Length : 4096; - using var reader = new StreamReader(_stream, Encoding.UTF8, false, (int)Math.Max(2, length), true); + var length = stream.CanSeek ? stream.Length : 4096; + using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)Math.Max(2, length), true); using var jsonTextReader = new JsonTextReader(reader); try { - _token = JToken.Load(jsonTextReader); + _token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false); IsJson = true; } catch (Exception) @@ -284,8 +292,12 @@ namespace CryptoExchange.Net.Converters.JsonNet public bool Read(ReadOnlyMemory data) { _bytes = data; - using var stream = new MemoryStream(data.ToArray()); - using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)Math.Max(2, data.Length), true); + + // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy + using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) + ? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) + : new MemoryStream(data.ToArray()); + using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); using var jsonTextReader = new JsonTextReader(reader); try @@ -303,7 +315,13 @@ namespace CryptoExchange.Net.Converters.JsonNet } /// - public override string GetOriginalString() => Encoding.UTF8.GetString(_bytes.ToArray()); + public override string GetOriginalString() => + // Netstandard 2.0 doesn't support GetString from a ReadonlySpan, so use ToArray there instead +#if NETSTANDARD2_0 + Encoding.UTF8.GetString(_bytes.ToArray()); +#else + Encoding.UTF8.GetString(_bytes.Span); +#endif /// public override bool OriginalDataAvailable => true; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs index 11eca74..d5a14f6 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs @@ -192,9 +192,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson [return: NotNullIfNotNull("enumValue")] public static string? GetString(T enumValue) => GetString(typeof(T), enumValue); - + /// + /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned + /// + /// + /// + /// [return: NotNullIfNotNull("enumValue")] - private static string? GetString(Type objectType, object? enumValue) + public static string? GetString(Type objectType, object? enumValue) { objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index c65be56..3910896 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Json; +using System.Threading.Tasks; namespace CryptoExchange.Net.Converters.SystemTextJson { @@ -197,22 +198,28 @@ namespace CryptoExchange.Net.Converters.SystemTextJson public override bool OriginalDataAvailable => _stream?.CanSeek == true; /// - public bool Read(Stream stream, bool bufferStream) + public async Task Read(Stream stream, bool bufferStream) { if (bufferStream && stream is not MemoryStream) { + // We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream _stream = new MemoryStream(); stream.CopyTo(_stream); _stream.Position = 0; } + else if (bufferStream) + { + // We need to buffer the stream, and the current stream is seekable, store as is + _stream = stream; + } else { - _stream = stream; + // We don't need to buffer the stream, so don't bother keeping the reference } try { - _document = JsonDocument.Parse(_stream); + _document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false); IsJson = true; } catch (Exception) @@ -271,7 +278,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } /// - public override string GetOriginalString() => Encoding.UTF8.GetString(_bytes.ToArray()); + public override string GetOriginalString() => + // Netstandard 2.0 doesn't support GetString from a ReadonlySpan, so use ToArray there instead +#if NETSTANDARD2_0 + Encoding.UTF8.GetString(_bytes.ToArray()); +#else + Encoding.UTF8.GetString(_bytes.Span); +#endif /// public override bool OriginalDataAvailable => true; diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 0ec990b..c28d6d0 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO.Compression; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Security; @@ -411,6 +413,22 @@ namespace CryptoExchange.Net return ub.Uri; } + + /// + /// Decompress using Gzip + /// + /// + /// + public static ReadOnlyMemory DecompressGzip(this ReadOnlyMemory data) + { + using var decompressedStream = new MemoryStream(); + using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment) + ? new MemoryStream(arraySegment.Array, arraySegment.Offset, arraySegment.Count) + : new MemoryStream(data.ToArray()); + using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); + deflateStream.CopyTo(decompressedStream); + return new ReadOnlyMemory(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); + } } } diff --git a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs index 44d3d70..7916c18 100644 --- a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces { @@ -83,7 +84,7 @@ namespace CryptoExchange.Net.Interfaces /// /// /// - bool Read(Stream stream, bool bufferStream); + Task Read(Stream stream, bool bufferStream); } /// diff --git a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs b/CryptoExchange.Net/Interfaces/IMessageProcessor.cs index b1846e1..cccc740 100644 --- a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageProcessor.cs @@ -38,7 +38,7 @@ namespace CryptoExchange.Net.Interfaces /// Type? GetMessageType(IMessageAccessor messageAccessor); /// - /// Deserialize a message int oobject of type + /// Deserialize a message into object of type /// /// /// diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 1a1382e..09a2a78 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -377,7 +377,8 @@ namespace CryptoExchange.Net.Sockets // 2. Read data into accessor _accessor.Read(data); - try { + try + { if (ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData) { originalData = _accessor.GetOriginalString(); @@ -400,7 +401,7 @@ namespace CryptoExchange.Net.Sockets lock (_listenersLock) processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList(); - if (!processors.Any()) + if (processors.Count == 0) { if (!ApiClient.UnhandledMessageExpected) {