diff --git a/src/Speckle.Sdk/Api/GraphQL/Serializer/NewtonsoftJsonSerializer.cs b/src/Speckle.Sdk/Api/GraphQL/Serializer/NewtonsoftJsonSerializer.cs index ce82b9a2..77915dc1 100644 --- a/src/Speckle.Sdk/Api/GraphQL/Serializer/NewtonsoftJsonSerializer.cs +++ b/src/Speckle.Sdk/Api/GraphQL/Serializer/NewtonsoftJsonSerializer.cs @@ -5,6 +5,7 @@ using GraphQL.Client.Abstractions; using GraphQL.Client.Abstractions.Websocket; using Speckle.Newtonsoft.Json; using Speckle.Newtonsoft.Json.Serialization; +using Speckle.Sdk.Serialisation; namespace Speckle.Sdk.Api.GraphQL.Serializer; @@ -73,7 +74,7 @@ internal sealed class NewtonsoftJsonSerializer : IGraphQLWebsocketJsonSerializer private Task DeserializeFromUtf8Stream(System.IO.Stream stream) { using var sr = new StreamReader(stream); - using JsonReader reader = new JsonTextReader(sr); + using JsonReader reader = SpeckleObjectSerializerPool.Instance.GetJsonTextReader(sr); var serializer = JsonSerializer.Create(JsonSerializerSettings); return Task.FromResult(serializer.Deserialize(reader)); } diff --git a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs index 51d11e18..8997b23a 100644 --- a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs +++ b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs @@ -156,9 +156,7 @@ public partial class Operations } // Shoot out the total children count, wasteful - var count = ( - await ClosureParser.GetClosuresAsync(objString, localTransport.CancellationToken).ConfigureAwait(false) - ).Count; + var count = ClosureParser.GetClosures(objString).Count; onTotalChildrenCountKnown?.Invoke(count); diff --git a/src/Speckle.Sdk/Helpers/SerializerIdWriter.cs b/src/Speckle.Sdk/Helpers/SerializerIdWriter.cs index afaa1fcc..5a8df3ac 100644 --- a/src/Speckle.Sdk/Helpers/SerializerIdWriter.cs +++ b/src/Speckle.Sdk/Helpers/SerializerIdWriter.cs @@ -1,4 +1,5 @@ using Speckle.Newtonsoft.Json; +using Speckle.Sdk.Serialisation; namespace Speckle.Sdk.Helpers; @@ -14,7 +15,7 @@ public sealed class SerializerIdWriter : JsonWriter { _jsonWriter = jsonWriter; _idWriter = new StringWriter(); - _jsonIdWriter = new JsonTextWriter(_idWriter); + _jsonIdWriter = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(_idWriter); } public (string, JsonWriter) FinishIdWriter() diff --git a/src/Speckle.Sdk/Serialisation/BaseObjectDeserializerV2.cs b/src/Speckle.Sdk/Serialisation/BaseObjectDeserializerV2.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer.cs b/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer.cs index e5f87f90..76f291e9 100644 --- a/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer.cs +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer.cs @@ -17,7 +17,7 @@ public sealed class SpeckleObjectDeserializer private readonly object _callbackLock = new(); private readonly object?[] _invokeNull = [null]; - // id -> Base if already deserialized or id -> Task if was handled by a bg thread + // id -> Base if already deserialized or id -> ValueTask if was handled by a bg thread private ConcurrentDictionary? _deserializedObjects; /// @@ -46,7 +46,7 @@ public sealed class SpeckleObjectDeserializer /// was null /// cannot be deserialised to type // /// did not contain the required json objects (closures) - public async Task DeserializeAsync([NotNull] string? rootObjectJson) + public async ValueTask DeserializeAsync([NotNull] string? rootObjectJson) { if (_isBusy) { @@ -79,20 +79,19 @@ public sealed class SpeckleObjectDeserializer } } - private async Task DeserializeJsonAsyncInternal(string objectJson) + private async ValueTask DeserializeJsonAsyncInternal(string objectJson) { // Apparently this automatically parses DateTimes in strings if it matches the format: // JObject doc1 = JObject.Parse(objectJson); // This is equivalent code that doesn't parse datetimes: - using JsonReader reader = new JsonTextReader(new StringReader(objectJson)); - + using JsonTextReader reader = SpeckleObjectSerializerPool.Instance.GetJsonTextReader(new StringReader(objectJson)); reader.DateParseHandling = DateParseHandling.None; object? converted; try { - await reader.ReadAsync(CancellationToken).ConfigureAwait(false); + reader.Read(); converted = await ReadObjectAsync(reader, CancellationToken).ConfigureAwait(false); } catch (Exception ex) when (!ex.IsFatal() && ex is not OperationCanceledException) @@ -112,9 +111,9 @@ public sealed class SpeckleObjectDeserializer } //this should be buffered - private async Task> ReadArrayAsync(JsonReader reader, CancellationToken ct) + private async ValueTask> ReadArrayAsync(JsonReader reader, CancellationToken ct) { - await reader.ReadAsync(ct).ConfigureAwait(false); + reader.Read(); List retList = new(); while (reader.TokenType != JsonToken.EndArray) { @@ -127,14 +126,14 @@ public sealed class SpeckleObjectDeserializer { retList.Add(convertedValue); } - await reader.ReadAsync(ct).ConfigureAwait(false); //goes to next + reader.Read(); //goes to next } return retList; } - private async Task ReadObjectAsync(JsonReader reader, CancellationToken ct) + private async ValueTask ReadObjectAsync(JsonReader reader, CancellationToken ct) { - await reader.ReadAsync(ct).ConfigureAwait(false); + reader.Read(); Dictionary dict = new(); while (reader.TokenType != JsonToken.EndObject) { @@ -145,8 +144,8 @@ public sealed class SpeckleObjectDeserializer string propName = (reader.Value?.ToString()).NotNull(); if (propName == "__closure") { - await reader.ReadAsync(ct).ConfigureAwait(false); //goes to prop value - var closures = await ClosureParser.GetClosuresAsync(reader).ConfigureAwait(false); + reader.Read(); //goes to prop value + var closures = ClosureParser.GetClosures(reader); foreach (var closure in closures) { _ids.Add(closure.Item1); @@ -159,13 +158,13 @@ public sealed class SpeckleObjectDeserializer // https://linear.app/speckle/issue/CXPLA-54/when-deserializing-dont-allow-closures-that-arent-downloadable await TryGetDeserializedAsync(objId).ConfigureAwait(false); } - await reader.ReadAsync(ct).ConfigureAwait(false); //goes to next + reader.Read(); //goes to next continue; } - await reader.ReadAsync(ct).ConfigureAwait(false); //goes prop value + reader.Read(); //goes prop value object? convertedValue = await ReadPropertyAsync(reader, ct).ConfigureAwait(false); dict[propName] = convertedValue; - await reader.ReadAsync(ct).ConfigureAwait(false); //goes to next + reader.Read(); //goes to next } break; default: @@ -188,7 +187,7 @@ public sealed class SpeckleObjectDeserializer return Dict2Base(dict); } - private async Task TryGetDeserializedAsync(string objId) + private async ValueTask TryGetDeserializedAsync(string objId) { object? deserialized = null; _deserializedObjects.NotNull(); @@ -201,7 +200,23 @@ public sealed class SpeckleObjectDeserializer { try { - deserialized = task.Result; + deserialized = await task.ConfigureAwait(false); + } + catch (AggregateException ex) + { + throw new SpeckleDeserializeException("Failed to deserialize reference object", ex); + } + + if (_deserializedObjects.TryAdd(objId, deserialized)) + { + _currentCount++; + } + } + if (deserialized is ValueTask valueTask) + { + try + { + deserialized = await valueTask.ConfigureAwait(false); } catch (AggregateException ex) { @@ -235,7 +250,7 @@ public sealed class SpeckleObjectDeserializer return deserialized; } - private async Task ReadPropertyAsync(JsonReader reader, CancellationToken ct) + private async ValueTask ReadPropertyAsync(JsonReader reader, CancellationToken ct) { ct.ThrowIfCancellationRequested(); switch (reader.TokenType) diff --git a/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs index c3f04549..998ff450 100644 --- a/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer.cs @@ -251,7 +251,7 @@ public class SpeckleObjectSerializer } using var writer = new StringWriter(); - using var jsonWriter = new JsonTextWriter(writer); + using var jsonWriter = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(writer); string id = SerializeBaseObject(baseObj, jsonWriter, closure); var json = writer.ToString(); @@ -275,7 +275,7 @@ public class SpeckleObjectSerializer ObjectReference objRef = new() { referencedId = id }; using var writer2 = new StringWriter(); - using var jsonWriter2 = new JsonTextWriter(writer2); + using var jsonWriter2 = SpeckleObjectSerializerPool.Instance.GetJsonTextWriter(writer2); SerializeProperty(objRef, jsonWriter2); var json2 = writer2.ToString(); UpdateParentClosures(id); diff --git a/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializerPool.cs b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializerPool.cs new file mode 100644 index 00000000..90380013 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializerPool.cs @@ -0,0 +1,25 @@ +using System.Buffers; +using Speckle.Newtonsoft.Json; +using Speckle.Sdk.Common; + +namespace Speckle.Sdk.Serialisation; + +public class SpeckleObjectSerializerPool +{ + public static readonly SpeckleObjectSerializerPool Instance = new(); + + private SpeckleObjectSerializerPool() { } + + public JsonTextWriter GetJsonTextWriter(TextWriter writer) => new(writer) { ArrayPool = _charPool }; + + public JsonTextReader GetJsonTextReader(TextReader reader) => new(reader) { ArrayPool = _charPool }; + + private readonly SerializerPool _charPool = new(ArrayPool.Create(4096, 4096)); + + private class SerializerPool(ArrayPool pool) : IArrayPool + { + public T[] Rent(int minimumLength) => pool.Rent(minimumLength); + + public void Return(T[]? array) => pool.Return(array.NotNull()); + } +} diff --git a/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs b/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs index 70598054..2730897c 100644 --- a/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs +++ b/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs @@ -5,27 +5,26 @@ namespace Speckle.Sdk.Serialisation.Utilities; public static class ClosureParser { - public static async Task> GetClosuresAsync( - string rootObjectJson, - CancellationToken cancellationToken = default - ) + public static IReadOnlyList<(string, int)> GetClosures(string rootObjectJson) { try { - using JsonTextReader reader = new(new StringReader(rootObjectJson)); - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + using JsonTextReader reader = SpeckleObjectSerializerPool.Instance.GetJsonTextReader( + new StringReader(rootObjectJson) + ); + reader.Read(); while (reader.TokenType != JsonToken.EndObject) { switch (reader.TokenType) { case JsonToken.StartObject: { - var closureList = await ReadObjectAsync(reader, cancellationToken).ConfigureAwait(false); + var closureList = ReadObject(reader); return closureList; } default: - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - await reader.SkipAsync(cancellationToken).ConfigureAwait(false); + reader.Read(); + reader.Skip(); break; } } @@ -34,17 +33,12 @@ public static class ClosureParser return []; } - public static async Task> GetChildrenIdsAsync( - string rootObjectJson, - CancellationToken cancellationToken = default - ) => (await GetClosuresAsync(rootObjectJson, cancellationToken).ConfigureAwait(false)).Select(x => x.Item1); + public static IEnumerable GetChildrenIds(string rootObjectJson) => + GetClosures(rootObjectJson).Select(x => x.Item1); - private static async Task> ReadObjectAsync( - JsonTextReader reader, - CancellationToken cancellationToken - ) + private static IReadOnlyList<(string, int)> ReadObject(JsonTextReader reader) { - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + reader.Read(); while (reader.TokenType != JsonToken.EndObject) { switch (reader.TokenType) @@ -53,46 +47,46 @@ public static class ClosureParser { if (reader.Value as string == "__closure") { - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); //goes to prop vale - var closureList = await ReadClosureEnumerableAsync(reader).ConfigureAwait(false); + reader.Read(); //goes to prop vale + var closureList = ReadClosureEnumerable(reader); return closureList; } - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); //goes to prop vale - await reader.SkipAsync(cancellationToken).ConfigureAwait(false); - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); //goes to next + reader.Read(); //goes to prop vale + reader.Skip(); + reader.Read(); //goes to next } break; default: - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); - await reader.SkipAsync(cancellationToken).ConfigureAwait(false); - await reader.ReadAsync(cancellationToken).ConfigureAwait(false); + reader.Read(); + reader.Skip(); + reader.Read(); break; } } return []; } - public static async Task> GetClosuresAsync(JsonReader reader) + public static IReadOnlyList<(string, int)> GetClosures(JsonReader reader) { if (reader.TokenType != JsonToken.StartObject) { return Array.Empty<(string, int)>(); } - var closureList = await ReadClosureEnumerableAsync(reader).ConfigureAwait(false); + var closureList = ReadClosureEnumerable(reader); closureList.Sort((a, b) => b.Item2.CompareTo(a.Item2)); return closureList; } - private static async Task> ReadClosureEnumerableAsync(JsonReader reader) + private static List<(string, int)> ReadClosureEnumerable(JsonReader reader) { List<(string, int)> closureList = new(); - await reader.ReadAsync().ConfigureAwait(false); //startobject + reader.Read(); //startobject while (reader.TokenType != JsonToken.EndObject) { var childId = (reader.Value as string).NotNull(); // propertyName - int childMinDepth = (await reader.ReadAsInt32Async().ConfigureAwait(false)).NotNull(); //propertyValue - await reader.ReadAsync().ConfigureAwait(false); + int childMinDepth = (reader.ReadAsInt32()).NotNull(); //propertyValue + reader.Read(); closureList.Add((childId, childMinDepth)); } return closureList; diff --git a/src/Speckle.Sdk/Transports/ServerTransport.cs b/src/Speckle.Sdk/Transports/ServerTransport.cs index 47045f1f..cdd997ca 100644 --- a/src/Speckle.Sdk/Transports/ServerTransport.cs +++ b/src/Speckle.Sdk/Transports/ServerTransport.cs @@ -140,9 +140,7 @@ public sealed class ServerTransport : IServerTransport api.CancellationToken = CancellationToken; string? rootObjectJson = await api.DownloadSingleObject(StreamId, id, OnProgressAction).ConfigureAwait(false); - var allIds = ( - await ClosureParser.GetChildrenIdsAsync(rootObjectJson.NotNull(), CancellationToken).ConfigureAwait(false) - ).ToList(); + var allIds = ClosureParser.GetChildrenIds(rootObjectJson.NotNull()).ToList(); var childrenIds = allIds.Where(x => !x.Contains("blob:")); var blobIds = allIds.Where(x => x.Contains("blob:")).Select(x => x.Remove(0, 5)); diff --git a/src/Speckle.Sdk/Transports/TransportHelpers.cs b/src/Speckle.Sdk/Transports/TransportHelpers.cs index 7111d193..87705d16 100644 --- a/src/Speckle.Sdk/Transports/TransportHelpers.cs +++ b/src/Speckle.Sdk/Transports/TransportHelpers.cs @@ -30,7 +30,7 @@ public static class TransportHelpers targetTransport.SaveObject(id, parent); - var closures = (await ClosureParser.GetChildrenIdsAsync(parent, cancellationToken).ConfigureAwait(false)).ToList(); + var closures = ClosureParser.GetChildrenIds(parent).ToList(); onTotalChildrenCountKnown?.Invoke(closures.Count);