diff --git a/Directory.Packages.props b/Directory.Packages.props index 4ae02af2..6d1dd95c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,10 +8,12 @@ + + - - - + + + @@ -30,4 +32,4 @@ - + \ No newline at end of file diff --git a/Speckle.Sdk.sln b/Speckle.Sdk.sln index bb0e5bbd..5883f32e 100644 --- a/Speckle.Sdk.sln +++ b/Speckle.Sdk.sln @@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Sdk.Tests.Performance", "tests\Speckle.Sdk.Tests.Performance\Speckle.Sdk.Tests.Performance.csproj", "{870E3396-E6F7-43AE-B120-E651FA4F46BD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Sdk.Performance.Testing", "tests\Speckle.Sdk.Performance.Testing\Speckle.Sdk.Performance.Testing.csproj", "{F3423F2A-3C83-4F4A-94CF-5D2214BF6CC9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {870E3396-E6F7-43AE-B120-E651FA4F46BD}.Debug|Any CPU.Build.0 = Debug|Any CPU {870E3396-E6F7-43AE-B120-E651FA4F46BD}.Release|Any CPU.ActiveCfg = Release|Any CPU {870E3396-E6F7-43AE-B120-E651FA4F46BD}.Release|Any CPU.Build.0 = Release|Any CPU + {F3423F2A-3C83-4F4A-94CF-5D2214BF6CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3423F2A-3C83-4F4A-94CF-5D2214BF6CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3423F2A-3C83-4F4A-94CF-5D2214BF6CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3423F2A-3C83-4F4A-94CF-5D2214BF6CC9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {A413E196-3696-4F48-B635-04B5F76BF9C9} = {5CB96C27-FC5B-4A41-86B6-951AF99B8116} @@ -95,5 +101,6 @@ Global {4FB41A6D-D139-4111-8115-E3F9F6BEAF24} = {35047EE7-AD1D-4741-80A7-8F0E874718E9} {B623BD21-5CAA-43F9-A539-1835276C220E} = {DA2AED52-58F9-471E-8AD8-102FD36129E3} {870E3396-E6F7-43AE-B120-E651FA4F46BD} = {35047EE7-AD1D-4741-80A7-8F0E874718E9} + {F3423F2A-3C83-4F4A-94CF-5D2214BF6CC9} = {35047EE7-AD1D-4741-80A7-8F0E874718E9} EndGlobalSection EndGlobal diff --git a/src/Speckle.Objects/packages.lock.json b/src/Speckle.Objects/packages.lock.json index bf429349..a9e35e79 100644 --- a/src/Speckle.Objects/packages.lock.json +++ b/src/Speckle.Objects/packages.lock.json @@ -168,8 +168,8 @@ }, "System.Buffers": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw==" + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" }, "System.ComponentModel.Annotations": { "type": "Transitive", @@ -178,12 +178,12 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", "dependencies": { - "System.Buffers": "4.4.0", + "System.Buffers": "4.5.1", "System.Numerics.Vectors": "4.4.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2" + "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, "System.Numerics.Vectors": { @@ -238,6 +238,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -290,6 +292,21 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )", @@ -498,6 +515,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -550,6 +569,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )", diff --git a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs index 9c3e7536..0354844e 100644 --- a/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs +++ b/src/Speckle.Sdk/Api/Operations/Operations.Receive.cs @@ -140,9 +140,7 @@ public async Task Receive( } // 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/Api/Operations/Operations.Send.cs b/src/Speckle.Sdk/Api/Operations/Operations.Send.cs index 7670484e..4f55c302 100644 --- a/src/Speckle.Sdk/Api/Operations/Operations.Send.cs +++ b/src/Speckle.Sdk/Api/Operations/Operations.Send.cs @@ -142,7 +142,7 @@ internal static async Task SerializerSend( ) { string obj = serializer.Serialize(value); - Task[] transportAwaits = serializer.WriteTransports.Select(t => t.WriteComplete()).ToArray(); + Task[] transportAwaits = serializer.WriteTransports.Select(t => t.WriteComplete().AsTask()).ToArray(); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Speckle.Sdk/Api/Operations/Operations.Serialize.cs b/src/Speckle.Sdk/Api/Operations/Operations.Serialize.cs index 1c2e3cd3..2efb8355 100644 --- a/src/Speckle.Sdk/Api/Operations/Operations.Serialize.cs +++ b/src/Speckle.Sdk/Api/Operations/Operations.Serialize.cs @@ -24,6 +24,12 @@ public string Serialize(Base value, CancellationToken cancellationToken = defaul return serializer.Serialize(value); } + public string Serialize2(Base value, CancellationToken cancellationToken = default) + { + var serializer = new SpeckleObjectSerializer2 { CancellationToken = cancellationToken }; + return serializer.Serialize(value); + } + /// /// Note: if you want to pull an object from a Speckle Transport or Server, /// please use diff --git a/src/Speckle.Sdk/Helpers/SerializerIdWriter2.cs b/src/Speckle.Sdk/Helpers/SerializerIdWriter2.cs new file mode 100644 index 00000000..639d851e --- /dev/null +++ b/src/Speckle.Sdk/Helpers/SerializerIdWriter2.cs @@ -0,0 +1,622 @@ +using Speckle.Newtonsoft.Json; + +namespace Speckle.Sdk.Helpers; + +public sealed class SerializerIdWriter2 : JsonWriter +{ + private readonly JsonWriter _jsonWriter; +#pragma warning disable CA2213 + private readonly JsonTextWriter _jsonIdWriter; +#pragma warning restore CA2213 + private readonly StringWriter _idWriter; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _idWriter.Dispose(); + } + + public SerializerIdWriter2(JsonWriter jsonWriter, SpeckleObjectSerializer2Pool pool) + { + _jsonWriter = jsonWriter; + _idWriter = new StringWriter(); + _jsonIdWriter = pool.GetJsonTextWriter(_idWriter); + } + + public string FinishIdWriter() + { + _jsonIdWriter.WriteEndObject(); + _jsonIdWriter.Flush(); + var json = _idWriter.ToString(); + ((IDisposable)_jsonIdWriter).Dispose(); + return json; + } + + public override void WriteValue(string? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteStartArray() + { + _jsonWriter.WriteStartArray(); + _jsonIdWriter.WriteStartArray(); + } + + public override void WriteEndArray() + { + _jsonWriter.WriteEndArray(); + _jsonIdWriter.WriteEndArray(); + } + + public override void WriteStartObject() + { + _jsonWriter.WriteStartObject(); + _jsonIdWriter.WriteStartObject(); + } + + public override void WriteEndObject() + { + _jsonWriter.WriteEndObject(); + _jsonIdWriter.WriteEndObject(); + } + + public override void WriteComment(string? text) + { + _jsonWriter.WriteComment(text); + _jsonIdWriter.WriteComment(text); + } + + public override void WritePropertyName(string name) + { + _jsonWriter.WritePropertyName(name); + _jsonIdWriter.WritePropertyName(name); + } + + public override void WriteNull() + { + _jsonWriter.WriteNull(); + _jsonIdWriter.WriteNull(); + } + + public override void WriteUndefined() + { + _jsonWriter.WriteUndefined(); + _jsonIdWriter.WriteUndefined(); + } + + public override void WriteRaw(string? json) + { + _jsonWriter.WriteRaw(json); + _jsonIdWriter.WriteRaw(json); + } + + public override void WriteRawValue(string? json) + { + _jsonWriter.WriteRawValue(json); + _jsonIdWriter.WriteRawValue(json); + } + + public override void WriteValue(bool value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(bool? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(byte value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(byte? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(char value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(char? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(DateTime value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(DateTime? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(DateTimeOffset value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(DateTimeOffset? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(decimal value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(decimal? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(double value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(double? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(float value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(float? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(Guid value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(Guid? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(int value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(int? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(long value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(long? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(sbyte value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(sbyte? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(short value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(short? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(TimeSpan value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(TimeSpan? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(uint value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(uint? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(ulong value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(ulong? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(Uri? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(ushort value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(ushort? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(byte[]? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void WriteValue(object? value) + { + _jsonWriter.WriteValue(value); + _jsonIdWriter.WriteValue(value); + } + + public override void Flush() + { + _jsonWriter.Flush(); + _jsonIdWriter.Flush(); + } + + public override async Task WriteValueAsync(string? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStartArrayAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteStartArrayAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteStartArrayAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteEndArrayAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteEndArrayAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteEndArrayAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStartObjectAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteStartObjectAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteStartObjectAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteEndObjectAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteEndObjectAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteEndObjectAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteCommentAsync(string? text, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteCommentAsync(text, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteCommentAsync(text, cancellationToken).ConfigureAwait(false); + } + + public override async Task WritePropertyNameAsync(string name, CancellationToken cancellationToken = default) + { + await _jsonWriter.WritePropertyNameAsync(name, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WritePropertyNameAsync(name, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteNullAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteNullAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteNullAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteUndefinedAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteUndefinedAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteUndefinedAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteRawAsync(string? json, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteRawAsync(json, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteRawAsync(json, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteRawValueAsync(string? json, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteRawValueAsync(json, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteRawValueAsync(json, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(bool value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(bool? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(byte value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(byte? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(char value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(char? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(DateTime value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(DateTime? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(DateTimeOffset value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(DateTimeOffset? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(decimal value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(decimal? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(double value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(double? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(float value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(float? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(Guid value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(Guid? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(int value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(int? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(long value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(long? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(sbyte value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(sbyte? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(short value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(short? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(TimeSpan value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(TimeSpan? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(uint value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(uint? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(ulong value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(ulong? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(Uri? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(ushort value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(ushort? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(byte[]? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteValueAsync(object? value, CancellationToken cancellationToken = default) + { + await _jsonWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.WriteValueAsync(value, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancellationToken = default) + { + await _jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + await _jsonIdWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Speckle.Sdk/Helpers/SpeckleObjectSerializer2Pool.cs b/src/Speckle.Sdk/Helpers/SpeckleObjectSerializer2Pool.cs new file mode 100644 index 00000000..7223797c --- /dev/null +++ b/src/Speckle.Sdk/Helpers/SpeckleObjectSerializer2Pool.cs @@ -0,0 +1,40 @@ +using System.Buffers; +using Microsoft.Extensions.ObjectPool; +using Speckle.Newtonsoft.Json; +using Speckle.Sdk.Common; + +namespace Speckle.Sdk.Helpers; + +public class SpeckleObjectSerializer2Pool +{ + public static readonly SpeckleObjectSerializer2Pool Instance = new(); + + private SpeckleObjectSerializer2Pool() { } + + 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()); + } + + public ObjectPool> DictPool { get; } = + new DefaultObjectPoolProvider().Create(new DictPoolPolicy()); + + private class DictPoolPolicy : PooledObjectPolicy> + { + public override Dictionary Create() => new(StringComparer.OrdinalIgnoreCase); + + public override bool Return(Dictionary obj) + { + obj.Clear(); + return true; + } + } +} 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 84b99404..97e79af2 100644 --- a/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer.cs +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer.cs @@ -139,7 +139,7 @@ public async Task DeserializeAsync(string rootObjectJson) if (propName == "__closure") { await reader.ReadAsync(ct).ConfigureAwait(false); //goes to prop value - var closures = await ClosureParser.GetClosuresAsync(reader).ConfigureAwait(false); + var closures = ClosureParser.GetClosures(reader); foreach (var closure in closures) { _ids.Add(closure.Item1); diff --git a/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer2.cs b/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer2.cs new file mode 100644 index 00000000..03dec3c0 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectDeserializer2.cs @@ -0,0 +1,352 @@ +using System.Collections.Concurrent; +using System.Numerics; +using System.Reflection; +using Speckle.Newtonsoft.Json; +using Speckle.Sdk.Common; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation.Utilities; +using Speckle.Sdk.Transports; + +namespace Speckle.Sdk.Serialisation; + +public sealed class SpeckleObjectDeserializer2 +{ + public SpeckleObjectSerializer2Pool Pool { get; } = SpeckleObjectSerializer2Pool.Instance; + private bool _isBusy; + 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 + private ConcurrentDictionary? _deserializedObjects; + + /// + /// Property that describes the type of the object. + /// + private const string TYPE_DISCRIMINATOR = nameof(Base.speckle_type); + + public CancellationToken CancellationToken { get; set; } + + /// + /// The sync transport. This transport will be used synchronously. + /// + public ITransport ReadTransport { get; set; } + + public Action? OnProgressAction { get; set; } + + private long _currentCount; + private readonly HashSet _ids = new(); + private long _processedCount; + + public string? BlobStorageFolder { get; set; } + + /// The JSON string of the object to be deserialized + /// A typed object deserialized from the + /// Thrown when + /// was null + /// cannot be deserialised to type + // /// did not contain the required json objects (closures) + public async ValueTask DeserializeAsync(string rootObjectJson) + { + if (_isBusy) + { + throw new InvalidOperationException( + "A deserializer instance can deserialize only 1 object at a time. Consider creating multiple deserializer instances" + ); + } + + try + { + _isBusy = true; + _deserializedObjects = new(StringComparer.Ordinal); + _currentCount = 0; + return (Base)(await DeserializeJsonAsyncInternal(rootObjectJson).NotNull().ConfigureAwait(false)); + } + finally + { + _deserializedObjects = null; + _isBusy = false; + } + } + + private async ValueTask DeserializeJsonAsyncInternal(string objectJson) + { + if (objectJson is null) + { + throw new ArgumentNullException(nameof(objectJson), $"Cannot deserialize {nameof(objectJson)}, value was null"); + } + // 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 = Pool.GetJsonTextReader(new StringReader(objectJson)); + + reader.DateParseHandling = DateParseHandling.None; + + object? converted; + try + { + reader.Read(); + converted = await ReadObjectAsync(reader, CancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsFatal() && ex is not OperationCanceledException) + { + throw new SpeckleDeserializeException($"Failed to deserialize", ex); + } + + lock (_callbackLock) + { + _processedCount++; + OnProgressAction?.Invoke( + new ProgressArgs(ProgressEvent.DeserializeObject, _currentCount, _ids.Count, _processedCount) + ); + } + + return converted; + } + + //this should be buffered + private async ValueTask> ReadArrayAsync(JsonReader reader, CancellationToken ct) + { + reader.Read(); + List retList = new(); + while (reader.TokenType != JsonToken.EndArray) + { + object? convertedValue = await ReadPropertyAsync(reader, ct).ConfigureAwait(false); + if (convertedValue is DataChunk chunk) + { + retList.AddRange(chunk.data); + } + else + { + retList.Add(convertedValue); + } + reader.Read(); //goes to next + } + return retList; + } + + private async ValueTask ReadObjectAsync(JsonReader reader, CancellationToken ct) + { + reader.Read(); + Dictionary dict = Pool.DictPool.Get(); + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + { + string propName = (reader.Value?.ToString()).NotNull(); + if (propName == "__closure") + { + reader.Read(); //goes to prop value + var closures = ClosureParser.GetClosures(reader); + foreach (var closure in closures) + { + _ids.Add(closure.Item1); + } + + foreach (var closure in closures) + { + string objId = closure.Item1; + //don't do anything with return value but later check if null + // https://linear.app/speckle/issue/CXPLA-54/when-deserializing-dont-allow-closures-that-arent-downloadable + await TryGetDeserializedAsync(objId).ConfigureAwait(false); + } + reader.Read(); //goes to next + continue; + } + reader.Read(); //goes prop value + object? convertedValue = await ReadPropertyAsync(reader, ct).ConfigureAwait(false); + dict[propName] = convertedValue; + reader.Read(); //goes to next + } + break; + default: + throw new InvalidOperationException($"Unknown {reader.ValueType} with {reader.Value}"); + } + } + + if (!dict.TryGetValue(TYPE_DISCRIMINATOR, out object? speckleType)) + { + return dict; + } + + if (speckleType as string == "reference" && dict.TryGetValue("referencedId", out object? referencedId)) + { + var objId = (string)referencedId.NotNull(); + object? deserialized = await TryGetDeserializedAsync(objId).ConfigureAwait(false); + return deserialized; + } + + var b = Dict2Base(dict); + Pool.DictPool.Return(dict); + return b; + } + + private async ValueTask TryGetDeserializedAsync(string objId) + { + object? deserialized = null; + _deserializedObjects.NotNull(); + if (_deserializedObjects.TryGetValue(objId, out object? o)) + { + deserialized = o; + } + + if (deserialized is ValueTask valueTask) + { + try + { + deserialized = valueTask.Result; + } + catch (AggregateException ex) + { + throw new SpeckleDeserializeException("Failed to deserialize reference object", ex); + } + + if (_deserializedObjects.TryAdd(objId, deserialized)) + { + _currentCount++; + } + } + if (deserialized is Task task) + { + try + { + deserialized = task.Result; + } + catch (AggregateException ex) + { + throw new SpeckleDeserializeException("Failed to deserialize reference object", ex); + } + + if (_deserializedObjects.TryAdd(objId, deserialized)) + { + _currentCount++; + } + } + if (deserialized != null) + { + return deserialized; + } + + // This reference was not already deserialized. Do it now in sync mode + string? objectJson = await ReadTransport.GetObject(objId).ConfigureAwait(false); + if (objectJson is null) + { + return null; + } + + deserialized = await DeserializeJsonAsyncInternal(objectJson).ConfigureAwait(false); + + if (_deserializedObjects.NotNull().TryAdd(objId, deserialized)) + { + _currentCount++; + } + + return deserialized; + } + + private async ValueTask ReadPropertyAsync(JsonReader reader, CancellationToken ct) + { + switch (reader.TokenType) + { + case JsonToken.Undefined: + case JsonToken.Null: + case JsonToken.None: + return null; + case JsonToken.Boolean: + return (bool)reader.Value.NotNull(); + case JsonToken.Integer: + if (reader.Value is long longValue) + { + return longValue; + } + if (reader.Value is BigInteger bitInt) + { + // This is behaviour carried over from v2 to facilitate large numbers from Python + // This is quite hacky, as it's a bit questionable exactly what numbers are supported, and with what tolerance + // For this reason, this can be considered undocumented behaviour, and is only for values within the range of a 64bit integer. + return (double)bitInt; + } + + throw new ArgumentException( + $"Found an unsupported integer type {reader.Value?.GetType()} with value {reader.Value}" + ); + case JsonToken.Float: + return (double)reader.Value.NotNull(); + case JsonToken.String: + return (string)reader.Value.NotNull(); + case JsonToken.Date: + return (DateTime)reader.Value.NotNull(); + case JsonToken.StartArray: + return await ReadArrayAsync(reader, ct).ConfigureAwait(false); + case JsonToken.StartObject: + var dict = await ReadObjectAsync(reader, ct).ConfigureAwait(false); + return dict; + + default: + throw new ArgumentException("Json value not supported: " + reader.ValueType); + } + } + + private Base Dict2Base(Dictionary dictObj) + { + string typeName = (string)dictObj[TYPE_DISCRIMINATOR].NotNull(); + Type type = TypeLoader.GetType(typeName); + Base baseObj = (Base)Activator.CreateInstance(type).NotNull(); + + dictObj.Remove(TYPE_DISCRIMINATOR); + dictObj.Remove("__closure"); + + var staticProperties = TypeCache.GetTypeProperties(typeName); + foreach (var entry in dictObj) + { + if (staticProperties.TryGetValue(entry.Key, out PropertyInfo? value) && value.CanWrite) + { + if (entry.Value == null) + { + // Check for JsonProperty(NullValueHandling = NullValueHandling.Ignore) attribute + JsonPropertyAttribute? attr = TypeLoader.GetJsonPropertyAttribute(value); + if (attr is { NullValueHandling: NullValueHandling.Ignore }) + { + continue; + } + } + + Type targetValueType = value.PropertyType; + bool conversionOk = ValueConverter.ConvertValue(targetValueType, entry.Value, out object? convertedValue); + if (conversionOk) + { + value.SetValue(baseObj, convertedValue); + } + else + { + // Cannot convert the value in the json to the static property type + throw new SpeckleDeserializeException( + $"Cannot deserialize {entry.Value?.GetType().FullName} to {targetValueType.FullName}" + ); + } + } + else + { + // No writable property with this name + CallSiteCache.SetValue(entry.Key, baseObj, entry.Value); + } + } + + if (baseObj is Blob bb && BlobStorageFolder != null) + { + bb.filePath = bb.GetLocalDestinationPath(BlobStorageFolder); + } + + var onDeserializedCallbacks = TypeCache.GetOnDeserializedCallbacks(typeName); + foreach (MethodInfo onDeserialized in onDeserializedCallbacks) + { + onDeserialized.Invoke(baseObj, _invokeNull); + } + + return baseObj; + } +} diff --git a/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer2.cs b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer2.cs new file mode 100644 index 00000000..d5d22057 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/SpeckleObjectSerializer2.cs @@ -0,0 +1,562 @@ +using System.Collections; +using System.Diagnostics.Contracts; +using System.Drawing; +using System.Globalization; +using System.Reflection; +using Speckle.DoubleNumerics; +using Speckle.Newtonsoft.Json; +using Speckle.Sdk.Common; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Models; +using Speckle.Sdk.Transports; +using Constants = Speckle.Sdk.Helpers.Constants; + +namespace Speckle.Sdk.Serialisation; + +public class SpeckleObjectSerializer2 +{ + private volatile bool _isBusy; + private List> _parentClosures = new(); + private HashSet _parentObjects = new(); + private readonly Dictionary> _typedPropertiesCache = new(); + private readonly Action? _onProgressAction; + + private readonly bool _trackDetachedChildren; + private int _serializedCount; + + /// + /// Keeps track of all detached children created during serialisation that have an applicationId (provided this serializer instance has been told to track detached children). + /// This is currently used to cache previously converted objects and avoid their conversion if they haven't changed. See the DUI3 send bindings in rhino or another host app. + /// + public Dictionary ObjectReferences { get; } = new(); + + /// The sync transport. This transport will be used synchronously. + public IReadOnlyCollection WriteTransports { get; } + + public SpeckleObjectSerializer2Pool Pool { get; } = SpeckleObjectSerializer2Pool.Instance; + + public CancellationToken CancellationToken { get; set; } + + public SpeckleObjectSerializer2() + : this(Array.Empty()) { } + + /// + /// Creates a new Serializer instance. + /// + /// The transports detached children should be persisted to. + /// Used to track progress. + /// Whether to store all detachable objects while serializing. They can be retrieved via post serialization. + /// + public SpeckleObjectSerializer2( + IReadOnlyCollection writeTransports, + Action? onProgressAction = null, + bool trackDetachedChildren = false, + CancellationToken cancellationToken = default + ) + { + WriteTransports = writeTransports; + _onProgressAction = onProgressAction; + CancellationToken = cancellationToken; + _trackDetachedChildren = trackDetachedChildren; + } + + /// The object to serialize + /// The serialized JSON + /// The serializer is busy (already serializing an object) + /// Failed to save object in one or more + /// Failed to extract (pre-serialize) properties from the + /// One or more 's cancellation token requested cancel + public string Serialize(Base baseObj) + { + if (_isBusy) + { + throw new InvalidOperationException( + "A serializer instance can serialize only 1 object at a time. Consider creating multiple serializer instances" + ); + } + + try + { + _isBusy = true; + try + { + var result = SerializeBase(baseObj, true).NotNull(); + StoreObject(result.Id.NotNull(), result.Json); + return result.Json; + } + catch (Exception ex) when (!ex.IsFatal() && ex is not OperationCanceledException) + { + throw new SpeckleSerializeException($"Failed to extract (pre-serialize) properties from the {baseObj}", ex); + } + } + finally + { + _parentClosures = new List>(); // cleanup in case of exceptions + _parentObjects = new HashSet(); + _isBusy = false; + } + } + + // `Preserialize` means transforming all objects into the final form that will appear in json, with basic .net objects + // (primitives, lists and dictionaries with string keys) + private void SerializeProperty( + object? obj, + JsonWriter writer, + bool computeClosures = false, + PropertyAttributeInfo inheritedDetachInfo = default + ) + { + CancellationToken.ThrowIfCancellationRequested(); + + if (obj == null) + { + writer.WriteNull(); + return; + } + + if (obj.GetType().IsPrimitive || obj is string) + { + writer.WriteValue(obj); + return; + } + + switch (obj) + { + // Start with object references so they're not captured by the Base class case below + // Note: this change was needed as we've made the ObjectReference type inherit from Base for + // the purpose of the "do not convert unchanged previously converted objects" POC. + case ObjectReference r: + Dictionary ret = + new() + { + ["speckle_type"] = r.speckle_type, + ["referencedId"] = r.referencedId, + ["__closure"] = r.closure + }; + if (r.closure is not null) + { + foreach (var kvp in r.closure) + { + UpdateParentClosures(kvp.Key); + } + } + UpdateParentClosures(r.referencedId); + SerializeProperty(ret, writer); + break; + case Base b: + var result = SerializeBase(b, computeClosures, inheritedDetachInfo); + if (result is not null) + { + writer.WriteRawValue(result.Json); + } + else + { + writer.WriteNull(); + } + break; + case IDictionary d: + { + writer.WriteStartObject(); + + foreach (DictionaryEntry kvp in d) + { + if (kvp.Key is not string key) + { + throw new ArgumentException( + "Serializing dictionaries that are not string based keys is not supported", + nameof(obj) + ); + } + + writer.WritePropertyName(key); + SerializeProperty(kvp.Value, writer, inheritedDetachInfo: inheritedDetachInfo); + } + writer.WriteEndObject(); + } + break; + case ICollection e: + { + writer.WriteStartArray(); + foreach (object? element in e) + { + SerializeProperty(element, writer, inheritedDetachInfo: inheritedDetachInfo); + } + writer.WriteEndArray(); + } + break; + case Enum: + writer.WriteValue((int)obj); + break; + // Support for simple types + case Guid g: + writer.WriteValue(g.ToString()); + break; + case Color c: + writer.WriteValue(c.ToArgb()); + break; + case DateTime t: + writer.WriteValue(t.ToString("o", CultureInfo.InvariantCulture)); + break; + case Matrix4x4 md: + writer.WriteStartArray(); + + writer.WriteValue(md.M11); + writer.WriteValue(md.M12); + writer.WriteValue(md.M13); + writer.WriteValue(md.M14); + writer.WriteValue(md.M21); + writer.WriteValue(md.M22); + writer.WriteValue(md.M23); + writer.WriteValue(md.M24); + writer.WriteValue(md.M31); + writer.WriteValue(md.M32); + writer.WriteValue(md.M33); + writer.WriteValue(md.M34); + writer.WriteValue(md.M41); + writer.WriteValue(md.M42); + writer.WriteValue(md.M43); + writer.WriteValue(md.M44); + writer.WriteEndArray(); + break; + //BACKWARDS COMPATIBILITY: matrix4x4 changed from System.Numerics float to System.DoubleNumerics double in release 2.16 + case System.Numerics.Matrix4x4: + throw new ArgumentException("Please use Speckle.DoubleNumerics.Matrix4x4 instead", nameof(obj)); + default: + throw new ArgumentException($"Unsupported value in serialization: {obj.GetType()}", nameof(obj)); + } + } + + internal SerializationResult? SerializeBase( + Base baseObj, + bool computeClosures = false, + PropertyAttributeInfo inheritedDetachInfo = default + ) + { + // handle circular references + bool alreadySerialized = !_parentObjects.Add(baseObj); + if (alreadySerialized) + { + return null; + } + + Dictionary closure = new(); + if (computeClosures || inheritedDetachInfo.IsDetachable || baseObj is Blob) + { + _parentClosures.Add(closure); + } + + string id; + string json; + using (var writer = new StringWriter()) + { + using var jsonWriter = Pool.GetJsonTextWriter(writer); + id = SerializeBaseObject(baseObj, jsonWriter, closure); + json = writer.ToString(); + } + + if (computeClosures || inheritedDetachInfo.IsDetachable || baseObj is Blob) + { + _parentClosures.RemoveAt(_parentClosures.Count - 1); + } + + _parentObjects.Remove(baseObj); + + if (baseObj is Blob myBlob) + { + StoreBlob(myBlob); + UpdateParentClosures($"blob:{id}"); + return new(json, id); + } + + if (inheritedDetachInfo.IsDetachable && WriteTransports.Count > 0) + { + StoreObject(id, json); + + ObjectReference objRef = new() { referencedId = id }; + + string json2; + using (var writer = new StringWriter()) + { + using var jsonWriter2 = Pool.GetJsonTextWriter(writer); + SerializeProperty(objRef, jsonWriter2); + json2 = writer.ToString(); + UpdateParentClosures(id); + } + + _onProgressAction?.Invoke(new(ProgressEvent.SerializeObject, ++_serializedCount, null)); + + // add to obj refs to return + if (baseObj.applicationId != null && _trackDetachedChildren) // && baseObj is not DataChunk && baseObj is not Abstract) // not needed, as data chunks will never have application ids, and abstract objs are not really used. + { + ObjectReferences[baseObj.applicationId] = new ObjectReference() + { + referencedId = id, + applicationId = baseObj.applicationId, + closure = closure + }; + } + return new(json2, null); + } + return new(json, id); + } + + private Dictionary ExtractAllProperties(Base baseObj) + { + IReadOnlyList<(PropertyInfo, PropertyAttributeInfo)> typedProperties = GetTypedPropertiesWithCache(baseObj); + IReadOnlyCollection dynamicProperties = baseObj.DynamicPropertyKeys; + + // propertyName -> (originalValue, isDetachable, isChunkable, chunkSize) + Dictionary allProperties = + new(typedProperties.Count + dynamicProperties.Count); + + // Construct `allProperties`: Add typed properties + foreach ((PropertyInfo propertyInfo, PropertyAttributeInfo detachInfo) in typedProperties) + { + object? baseValue = propertyInfo.GetValue(baseObj); + allProperties[propertyInfo.Name] = (baseValue, detachInfo); + } + + // Construct `allProperties`: Add dynamic properties + foreach (string propName in dynamicProperties) + { + if (propName.StartsWith("__")) + { + continue; + } + + object? baseValue = baseObj[propName]; +#if NETSTANDARD2_0 + bool isDetachable = propName.StartsWith("@"); +#else + bool isDetachable = propName.StartsWith('@'); +#endif + bool isChunkable = false; + int chunkSize = 1000; + + if (Constants.ChunkPropertyNameRegex.IsMatch(propName)) + { + var match = Constants.ChunkPropertyNameRegex.Match(propName); + isChunkable = int.TryParse(match.Groups[^1].Value, out chunkSize); + } + allProperties[propName] = (baseValue, new PropertyAttributeInfo(isDetachable, isChunkable, chunkSize, null)); + } + + return allProperties; + } + + private string SerializeBaseObject(Base baseObj, JsonWriter writer, IReadOnlyDictionary closure) + { + var allProperties = ExtractAllProperties(baseObj); + + SerializerIdWriter2? serializerIdWriter = null; + JsonWriter baseWriter = writer; + if (baseObj is not Blob) + { + serializerIdWriter = new SerializerIdWriter2(writer, Pool); + writer = serializerIdWriter; + } + + writer.WriteStartObject(); + // Convert all properties + foreach (var prop in allProperties) + { + if (prop.Value.info.JsonPropertyInfo is { NullValueHandling: NullValueHandling.Ignore }) + { + continue; + } + + writer.WritePropertyName(prop.Key); + SerializeProperty(prop.Value.value, writer, prop.Value.info); + } + + string id; + if (serializerIdWriter is not null) + { + var json = serializerIdWriter.FinishIdWriter(); + ((IDisposable)serializerIdWriter).Dispose(); + writer = baseWriter; + id = ComputeId(json); + } + else + { + id = ((Blob)baseObj).id; + } + writer.WritePropertyName("id"); + writer.WriteValue(id); + baseObj.id = id; + + if (closure.Count > 0) + { + writer.WritePropertyName("__closure"); + writer.WriteStartObject(); + foreach (var c in closure) + { + writer.WritePropertyName(c.Key); + writer.WriteValue(c.Value); + } + writer.WriteEndObject(); + } + + writer.WriteEndObject(); + return id; + } + + private void SerializeProperty(object? baseValue, JsonWriter jsonWriter, PropertyAttributeInfo detachInfo) + { + // If there are no WriteTransports, keep everything attached. + if (WriteTransports.Count == 0) + { + SerializeProperty(baseValue, jsonWriter, inheritedDetachInfo: detachInfo); + return; + } + + if (baseValue is IEnumerable chunkableCollection && detachInfo.IsChunkable) + { + List chunks = new(); + DataChunk crtChunk = new() { data = new List(detachInfo.ChunkSize) }; + + foreach (object element in chunkableCollection) + { + crtChunk.data.Add(element); + if (crtChunk.data.Count >= detachInfo.ChunkSize) + { + chunks.Add(crtChunk); + crtChunk = new DataChunk { data = new List(detachInfo.ChunkSize) }; + } + } + + if (crtChunk.data.Count > 0) + { + chunks.Add(crtChunk); + } + SerializeProperty(chunks, jsonWriter, inheritedDetachInfo: new PropertyAttributeInfo(true, false, 0, null)); + return; + } + + SerializeProperty(baseValue, jsonWriter, inheritedDetachInfo: detachInfo); + } + + private void UpdateParentClosures(string objectId) + { + for (int parentLevel = 0; parentLevel < _parentClosures.Count; parentLevel++) + { + int childDepth = _parentClosures.Count - parentLevel; + if (!_parentClosures[parentLevel].TryGetValue(objectId, out int currentValue)) + { + currentValue = childDepth; + } + + _parentClosures[parentLevel][objectId] = Math.Min(currentValue, childDepth); + } + } + + [Pure] + private static string ComputeId(string serialized) + { +#if NET6_0_OR_GREATER + string hash = Crypt.Sha256(serialized.AsSpan(), length: HashUtility.HASH_LENGTH); +#else + string hash = Crypt.Sha256(serialized, length: HashUtility.HASH_LENGTH); +#endif + return hash; + } + + private void StoreObject(string objectId, string objectJson) + { + foreach (var transport in WriteTransports) + { + transport.SaveObject(objectId, objectJson); + } + } + + private void StoreBlob(Blob obj) + { + bool hasBlobTransport = false; + + foreach (var transport in WriteTransports) + { + if (transport is IBlobCapableTransport blobTransport) + { + hasBlobTransport = true; + blobTransport.SaveBlob(obj); + } + } + if (!hasBlobTransport) + { + throw new InvalidOperationException( + "Object tree contains a Blob (file), but the serializer has no blob saving capable transports." + ); + } + } + + // (propertyInfo, isDetachable, isChunkable, chunkSize, JsonPropertyAttribute) + private IReadOnlyList<(PropertyInfo, PropertyAttributeInfo)> GetTypedPropertiesWithCache(Base baseObj) + { + Type type = baseObj.GetType(); + + if ( + _typedPropertiesCache.TryGetValue( + type.FullName.NotNull(), + out List<(PropertyInfo, PropertyAttributeInfo)>? cached + ) + ) + { + return cached; + } + + var typedProperties = baseObj.GetInstanceMembers().ToList(); + List<(PropertyInfo, PropertyAttributeInfo)> ret = new(typedProperties.Count); + + foreach (PropertyInfo typedProperty in typedProperties) + { + if (typedProperty.Name.StartsWith("__") || typedProperty.Name == "id") + { + continue; + } + + bool jsonIgnore = typedProperty.IsDefined(typeof(JsonIgnoreAttribute), false); + if (jsonIgnore) + { + continue; + } + + _ = typedProperty.GetValue(baseObj); + + List detachableAttributes = typedProperty + .GetCustomAttributes(true) + .ToList(); + List chunkableAttributes = typedProperty + .GetCustomAttributes(true) + .ToList(); + bool isDetachable = detachableAttributes.Count > 0 && detachableAttributes[0].Detachable; + bool isChunkable = chunkableAttributes.Count > 0; + int chunkSize = isChunkable ? chunkableAttributes[0].MaxObjCountPerChunk : 1000; + JsonPropertyAttribute? jsonPropertyAttribute = typedProperty.GetCustomAttribute(); + ret.Add((typedProperty, new PropertyAttributeInfo(isDetachable, isChunkable, chunkSize, jsonPropertyAttribute))); + } + + _typedPropertiesCache[type.FullName] = ret; + return ret; + } + + internal readonly struct PropertyAttributeInfo + { + public PropertyAttributeInfo( + bool isDetachable, + bool isChunkable, + int chunkSize, + JsonPropertyAttribute? jsonPropertyAttribute + ) + { + IsDetachable = isDetachable || isChunkable; + IsChunkable = isChunkable; + ChunkSize = chunkSize; + JsonPropertyInfo = jsonPropertyAttribute; + } + + public readonly bool IsDetachable; + public readonly bool IsChunkable; + public readonly int ChunkSize; + public readonly JsonPropertyAttribute? JsonPropertyInfo; + } +} diff --git a/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs b/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs index 70598054..7c2abd18 100644 --- a/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs +++ b/src/Speckle.Sdk/Serialisation/Utilities/ClosureParser.cs @@ -1,31 +1,31 @@ using Speckle.Newtonsoft.Json; using Speckle.Sdk.Common; +using Speckle.Sdk.Helpers; 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 = SpeckleObjectSerializer2Pool.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 +34,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 +48,46 @@ CancellationToken cancellationToken { 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/Speckle.Sdk.csproj b/src/Speckle.Sdk/Speckle.Sdk.csproj index 3575ac76..8a72687d 100644 --- a/src/Speckle.Sdk/Speckle.Sdk.csproj +++ b/src/Speckle.Sdk/Speckle.Sdk.csproj @@ -29,6 +29,8 @@ + + diff --git a/src/Speckle.Sdk/Transports/DiskTransport.cs b/src/Speckle.Sdk/Transports/DiskTransport.cs index 2b543e15..4b049bbc 100644 --- a/src/Speckle.Sdk/Transports/DiskTransport.cs +++ b/src/Speckle.Sdk/Transports/DiskTransport.cs @@ -59,17 +59,24 @@ public void BeginWrite() public void EndWrite() { } - public Task GetObject(string id) + public ValueTask GetObject(string id) { CancellationToken.ThrowIfCancellationRequested(); var filePath = Path.Combine(RootPath, id); if (File.Exists(filePath)) { - return Task.FromResult(File.ReadAllText(filePath, Encoding.UTF8)); +#if NETSTANDARD2_0 + return new ValueTask(File.ReadAllText(filePath, Encoding.UTF8)); +#else + return ValueTask.FromResult(File.ReadAllText(filePath, Encoding.UTF8)); +#endif } - - return Task.FromResult(null); +#if NETSTANDARD2_0 + return new ValueTask((string?)null); +#else + return ValueTask.FromResult(null); +#endif } public void SaveObject(string id, string serializedObject) @@ -98,12 +105,16 @@ public void SaveObject(string id, string serializedObject) Elapsed += stopwatch.Elapsed; } - public Task WriteComplete() + public ValueTask WriteComplete() { - return Task.CompletedTask; +#if NETSTANDARD2_0 + return new ValueTask(Task.CompletedTask); +#else + return ValueTask.CompletedTask; +#endif } - public async Task CopyObjectAndChildren( + public async ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action? onTotalChildrenCountKnown = null @@ -115,7 +126,7 @@ public async Task CopyObjectAndChildren( return res; } - public Task> HasObjects(IReadOnlyList objectIds) + public ValueTask> HasObjects(IReadOnlyList objectIds) { Dictionary ret = new(); foreach (string objectId in objectIds) @@ -123,7 +134,11 @@ public Task> HasObjects(IReadOnlyList objectIds var filePath = Path.Combine(RootPath, objectId); ret[objectId] = File.Exists(filePath); } - return Task.FromResult(ret); +#if NETSTANDARD2_0 + return new ValueTask>(ret); +#else + return ValueTask.FromResult(ret); +#endif } public override string ToString() diff --git a/src/Speckle.Sdk/Transports/ITransport.cs b/src/Speckle.Sdk/Transports/ITransport.cs index 5fe1c4b8..5485c658 100644 --- a/src/Speckle.Sdk/Transports/ITransport.cs +++ b/src/Speckle.Sdk/Transports/ITransport.cs @@ -67,12 +67,12 @@ public interface ITransport /// Awaitable method to figure out whether writing is completed. /// /// - public Task WriteComplete(); + public ValueTask WriteComplete(); /// The object's hash. /// The serialized object data, or if the transport cannot find the object /// requested cancel - public Task GetObject(string id); + public ValueTask GetObject(string id); /// /// Copies the parent object and all its children to the provided transport. @@ -84,7 +84,7 @@ public interface ITransport /// The provided arguments are not valid /// The transport could not complete the operation /// requested cancel - public Task CopyObjectAndChildren( + public ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action? onTotalChildrenCountKnown = null @@ -97,7 +97,7 @@ public Task CopyObjectAndChildren( /// A dictionary with the specified object ids as keys and boolean values, whether each object is present in the transport or not /// The transport could not complete the operation /// requested cancel - public Task> HasObjects(IReadOnlyList objectIds); + public ValueTask> HasObjects(IReadOnlyList objectIds); } public interface IBlobCapableTransport diff --git a/src/Speckle.Sdk/Transports/MemoryTransport.cs b/src/Speckle.Sdk/Transports/MemoryTransport.cs index f3825c7a..78b758c3 100644 --- a/src/Speckle.Sdk/Transports/MemoryTransport.cs +++ b/src/Speckle.Sdk/Transports/MemoryTransport.cs @@ -90,16 +90,21 @@ public void SaveObject(string id, string serializedObject) Elapsed += stopwatch.Elapsed; } - public Task GetObject(string id) + public ValueTask GetObject(string id) { var stopwatch = Stopwatch.StartNew(); var ret = Objects.TryGetValue(id, out string? o) ? o : null; stopwatch.Stop(); Elapsed += stopwatch.Elapsed; - return Task.FromResult(ret); + +#if NETSTANDARD2_0 + return new ValueTask(ret); +#else + return ValueTask.FromResult(ret); +#endif } - public async Task CopyObjectAndChildren( + public async ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action? onTotalChildrenCountKnown = null @@ -111,20 +116,27 @@ public async Task CopyObjectAndChildren( return res; } - public Task WriteComplete() + public ValueTask WriteComplete() { - return Task.CompletedTask; +#if NETSTANDARD2_0 + return new ValueTask(Task.CompletedTask); +#else + return ValueTask.CompletedTask; +#endif } - public Task> HasObjects(IReadOnlyList objectIds) + public ValueTask> HasObjects(IReadOnlyList objectIds) { Dictionary ret = new(objectIds.Count); foreach (string objectId in objectIds) { ret[objectId] = Objects.ContainsKey(objectId); } - - return Task.FromResult(ret); +#if NETSTANDARD2_0 + return new ValueTask>(ret); +#else + return ValueTask.FromResult(ret); +#endif } public override string ToString() diff --git a/src/Speckle.Sdk/Transports/SQLiteTransport.cs b/src/Speckle.Sdk/Transports/SQLiteTransport.cs index d0f8f732..01f5c753 100644 --- a/src/Speckle.Sdk/Transports/SQLiteTransport.cs +++ b/src/Speckle.Sdk/Transports/SQLiteTransport.cs @@ -129,7 +129,7 @@ public void BeginWrite() public void EndWrite() { } - public Task> HasObjects(IReadOnlyList objectIds) + public ValueTask> HasObjects(IReadOnlyList objectIds) { Dictionary ret = new(objectIds.Count); // Initialize with false so that canceled queries still return a dictionary item for every object id @@ -159,8 +159,11 @@ public Task> HasObjects(IReadOnlyList objectIds { throw new TransportException("SQLite transport failed", ex); } - - return Task.FromResult(ret); +#if NETSTANDARD2_0 + return new ValueTask>(ret); +#else + return ValueTask.FromResult(ret); +#endif } /// Failed to initialize connection to the SQLite DB @@ -272,7 +275,7 @@ public override string ToString() /// Awaits untill write completion (ie, the current queue is fully consumed). /// /// - public async Task WriteComplete() => + public async ValueTask WriteComplete() => await Utilities.WaitUntil(() => WriteCompletionStatus, 500).ConfigureAwait(false); /// @@ -400,7 +403,7 @@ public void SaveObjectSync(string hash, string serializedObject) /// /// /// - public async Task GetObject(string id) + public async ValueTask GetObject(string id) { CancellationToken.ThrowIfCancellationRequested(); await _connectionLock.WaitAsync(CancellationToken).ConfigureAwait(false); @@ -427,7 +430,7 @@ public void SaveObjectSync(string hash, string serializedObject) return null; // pass on the duty of null checks to consumers } - public async Task CopyObjectAndChildren( + public async ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action? onTotalChildrenCountKnown = null diff --git a/src/Speckle.Sdk/Transports/ServerTransport.cs b/src/Speckle.Sdk/Transports/ServerTransport.cs index 47045f1f..2c0eca02 100644 --- a/src/Speckle.Sdk/Transports/ServerTransport.cs +++ b/src/Speckle.Sdk/Transports/ServerTransport.cs @@ -120,7 +120,7 @@ public void Dispose() public Action? OnProgressAction { get; set; } public TimeSpan Elapsed { get; private set; } = TimeSpan.Zero; - public async Task CopyObjectAndChildren( + public async ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action? onTotalChildrenCountKnown = null @@ -140,9 +140,7 @@ public async Task CopyObjectAndChildren( 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)); @@ -201,7 +199,7 @@ await api.DownloadObjects( return rootObjectJson; } - public async Task GetObject(string id) + public async ValueTask GetObject(string id) { CancellationToken.ThrowIfCancellationRequested(); var stopwatch = Stopwatch.StartNew(); @@ -211,7 +209,7 @@ await api.DownloadObjects( return result; } - public async Task> HasObjects(IReadOnlyList objectIds) + public async ValueTask> HasObjects(IReadOnlyList objectIds) { return await Api.HasObjects(StreamId, objectIds).ConfigureAwait(false); } @@ -243,7 +241,7 @@ public void BeginWrite() _sendingThread.Start(); } - public async Task WriteComplete() + public async ValueTask WriteComplete() { while (true) { diff --git a/src/Speckle.Sdk/Transports/TransportHelpers.cs b/src/Speckle.Sdk/Transports/TransportHelpers.cs index 7111d193..c3ebf96d 100644 --- a/src/Speckle.Sdk/Transports/TransportHelpers.cs +++ b/src/Speckle.Sdk/Transports/TransportHelpers.cs @@ -5,7 +5,7 @@ namespace Speckle.Sdk.Transports; public static class TransportHelpers { - public static async Task CopyObjectAndChildrenAsync( + public static async ValueTask CopyObjectAndChildrenAsync( string id, ITransport sourceTransport, ITransport targetTransport, @@ -30,7 +30,7 @@ CancellationToken cancellationToken targetTransport.SaveObject(id, parent); - var closures = (await ClosureParser.GetChildrenIdsAsync(parent, cancellationToken).ConfigureAwait(false)).ToList(); + var closures = ClosureParser.GetChildrenIds(parent).ToList(); onTotalChildrenCountKnown?.Invoke(closures.Count); diff --git a/src/Speckle.Sdk/Transports/Utilities.cs b/src/Speckle.Sdk/Transports/Utilities.cs index 3fe06faf..478b3138 100644 --- a/src/Speckle.Sdk/Transports/Utilities.cs +++ b/src/Speckle.Sdk/Transports/Utilities.cs @@ -8,7 +8,7 @@ public static class Utilities /// /// /// - public static async Task WaitUntil(Func condition, int frequency = 25) + public static async ValueTask WaitUntil(Func condition, int frequency = 25) { while (!condition()) { diff --git a/src/Speckle.Sdk/packages.lock.json b/src/Speckle.Sdk/packages.lock.json index d7266b92..4a1135a2 100644 --- a/src/Speckle.Sdk/packages.lock.json +++ b/src/Speckle.Sdk/packages.lock.json @@ -53,6 +53,21 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Direct", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Direct", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", @@ -246,8 +261,8 @@ }, "System.Buffers": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw==" + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" }, "System.ComponentModel.Annotations": { "type": "Transitive", @@ -256,12 +271,12 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", "dependencies": { - "System.Buffers": "4.4.0", + "System.Buffers": "4.5.1", "System.Numerics.Vectors": "4.4.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2" + "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, "System.Numerics.Vectors": { @@ -361,6 +376,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "Direct", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Direct", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Microsoft.SourceLink.GitHub": { "type": "Direct", "requested": "[8.0.0, )", diff --git a/tests/Speckle.Objects.Tests.Unit/packages.lock.json b/tests/Speckle.Objects.Tests.Unit/packages.lock.json index e1d02f86..8d72263b 100644 --- a/tests/Speckle.Objects.Tests.Unit/packages.lock.json +++ b/tests/Speckle.Objects.Tests.Unit/packages.lock.json @@ -278,6 +278,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -330,6 +332,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )", diff --git a/tests/Speckle.Sdk.Performance.Testing/NullTransport.cs b/tests/Speckle.Sdk.Performance.Testing/NullTransport.cs new file mode 100644 index 00000000..2dd973a6 --- /dev/null +++ b/tests/Speckle.Sdk.Performance.Testing/NullTransport.cs @@ -0,0 +1,34 @@ +using Speckle.Sdk.Transports; + +namespace Speckle.Sdk.Performance.Testing; + +public class NullTransport : ITransport +{ + public string TransportName { get; set; } = ""; + public Dictionary TransportContext { get; } = new(); + public TimeSpan Elapsed { get; } = TimeSpan.Zero; + public CancellationToken CancellationToken { get; set; } + public Action? OnProgressAction { get; set; } + + public void BeginWrite() { } + + public void EndWrite() { } + + public void SaveObject(string id, string serializedObject) { } + + public ValueTask WriteComplete() + { + return ValueTask.CompletedTask; + } + + public ValueTask GetObject(string id) => throw new NotImplementedException(); + + public ValueTask CopyObjectAndChildren( + string id, + ITransport targetTransport, + Action? onTotalChildrenCountKnown = null + ) => throw new NotImplementedException(); + + public ValueTask> HasObjects(IReadOnlyList objectIds) => + throw new NotImplementedException(); +} diff --git a/tests/Speckle.Sdk.Performance.Testing/Program.cs b/tests/Speckle.Sdk.Performance.Testing/Program.cs new file mode 100644 index 00000000..25a87f2f --- /dev/null +++ b/tests/Speckle.Sdk.Performance.Testing/Program.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using Speckle.Sdk.Common; +using Speckle.Sdk.Credentials; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation; +using Speckle.Sdk.Tests.Performance; + +TypeLoader.Reset(); +TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly()); + +var url = "https://latest.speckle.systems/projects/a3ac1b2706/models/59d3b0f3c6"; //small? + +//var url = "https://latest.speckle.systems/projects/2099ac4b5f/models/da511c4d1e"; //perf? + +using var dataSource = new TestDataHelper(); +await dataSource + .SeedTransport(new Account() { serverInfo = new() { url = url } }, "2099ac4b5f", "30fb4cbe6eb2202b9e7b4a4fcc3dd2b6") + .ConfigureAwait(false); + +SpeckleObjectDeserializer2 deserializer = new() { ReadTransport = dataSource.Transport }; +string data = await dataSource.Transport.GetObject(dataSource.ObjectId).NotNull().ConfigureAwait(false); +var testData = await deserializer.DeserializeAsync(data).ConfigureAwait(false); + +Console.WriteLine("Attach"); +Console.ReadLine(); +Console.WriteLine("Executing"); +SpeckleObjectSerializer2 sut = new(); +var x = sut.Serialize(testData); +Console.WriteLine("Detach"); +Console.ReadLine(); diff --git a/tests/Speckle.Sdk.Performance.Testing/Speckle.Sdk.Performance.Testing.csproj b/tests/Speckle.Sdk.Performance.Testing/Speckle.Sdk.Performance.Testing.csproj new file mode 100644 index 00000000..000d4525 --- /dev/null +++ b/tests/Speckle.Sdk.Performance.Testing/Speckle.Sdk.Performance.Testing.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/tests/Speckle.Sdk.Performance.Testing/packages.lock.json b/tests/Speckle.Sdk.Performance.Testing/packages.lock.json new file mode 100644 index 00000000..e45aa284 --- /dev/null +++ b/tests/Speckle.Sdk.Performance.Testing/packages.lock.json @@ -0,0 +1,477 @@ +{ + "version": 2, + "dependencies": { + "net8.0": { + "GitVersion.MsBuild": { + "type": "Direct", + "requested": "[5.12.0, )", + "resolved": "5.12.0", + "contentHash": "dJuigXycpJNOiLT9or7mkHSkGFHgGW3/p6cNNYEKZBa7Hhp1FdX/cvqYWWYhRLpfoZOedeA7aRbYiOB3vW/dvA==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "PolySharp": { + "type": "Direct", + "requested": "[1.14.1, )", + "resolved": "1.14.1", + "contentHash": "mOOmFYwad3MIOL14VCjj02LljyF1GNw1wP0YVlxtcPvqdxjGGMNdNJJxHptlry3MOd8b40Flm8RPOM8JOlN2sQ==" + }, + "Speckle.InterfaceGenerator": { + "type": "Direct", + "requested": "[0.9.6, )", + "resolved": "0.9.6", + "contentHash": "HKH7tYrYYlCK1ct483hgxERAdVdMtl7gUKW9ijWXxA1UsYR4Z+TrRHYmzZ9qmpu1NnTycSrp005NYM78GDKV1w==" + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.14.0", + "contentHash": "CUDCg6bgHrDzhjnA+IOBl5gAo8Y5hZ2YSs7MBXrYMlMKpBZqrD5ez0537uDveOkcf+YWAoK+S4sMcuWPbIz8bw==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "GraphQL.Client.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==", + "dependencies": { + "GraphQL.Primitives": "6.0.0" + } + }, + "GraphQL.Client.Abstractions.Websocket": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==", + "dependencies": { + "GraphQL.Client.Abstractions": "6.0.0" + } + }, + "GraphQL.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.17.0", + "contentHash": "8x+HCVTl/HHTGpscH3vMBhV8sknN/muZFw9s3TsI8SA6+c43cOTCi2+jE4KsU8pNLbJ++iF2ZFcpcXHXtDglnw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg==" + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.3", + "contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bNzTyxP3iD5FPFHfVDl15Y6/wSoI7e3MeV0lOaj9igbIKTjgrmuw6LoVJ06jUNFA7+KaDC/OIsStWl/FQJz6sQ==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.3", + "System.Collections.Immutable": "5.0.0", + "System.Memory": "4.5.4", + "System.Reflection.Metadata": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "sbu6kDGzo9bfQxuqWpeEE7I9P30bSuZEnpDz9/qz20OU6pm79Z63+/BsAzO2e/R/Q97kBrpj647wokZnEVr97w==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.1.0]" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "7.0.7", + "contentHash": "21FRzcJhaTrlv7kTrqr/ltFcSQM2TyuTTPhUcjO8H73od7Bb3QraNW90c7lUucNI/245XPkKZG4fp7/7OsKCSg==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.4" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.251802", + "contentHash": "bqnYl6AdSeboeN4v25hSukK6Odm6/54E3Y2B8rBvgqvAW0mF8fo7XNRVE2DMOG7Rk0fiuA079QIH28+V+W1Zdg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.1.0", + "Microsoft.Extensions.Logging": "2.1.1" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "2.2.332302", + "contentHash": "Hp84ivxSKIMTBzYSATxmUsm3YSXHWivcwiRRbsydGmqujMUK8BAueLN0ssAVEOkOBmh0vjUBhrq7YcroT7VCug==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.251802", + "System.Collections.Immutable": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.8", + "contentHash": "kl3UMrZKSeSEYZ8rt/GjLUQToREjgQABqfg6PzQBmSlYHTZOKE9ePEOS2xptROQ9SVvngg3QGX51TIT11iZ0wA==", + "dependencies": { + "Microsoft.Win32.Registry": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "5.0.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "nOP8R1mVb/6mZtm2qgAJXn/LFm/2kMjHDAg/QJLFG6CuWYJtaD3p1BwQhufBVvRzL9ceJ/xF0SQ0qsI2GkDQAA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "2.2.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.2.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "vJ9xvOZCnUAIHcGC3SU35r3HKmHTVIeHzo6u/qzlHAqD8m6xv92MLin4oJntTvkpKxVX3vI1GFFkIQtU3AdlsQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "2.2.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "UpZLNLBpIZ0GTebShui7xXYh6DmBHjWM8NxGxZbdQh/bPZ5e6YswqI+bru6BnEL5eWiOdodsXtEz3FROcgi/qg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0", + "Microsoft.Extensions.Primitives": "2.2.0", + "System.ComponentModel.Annotations": "4.5.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==", + "dependencies": { + "System.Memory": "4.5.1", + "System.Runtime.CompilerServices.Unsafe": "4.5.1" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.3.17", + "contentHash": "FQgtCoF2HFwvzKWulAwBS5BGLlh8pgbrJtOp47jyBwh2CW16juVtacN1azOA2BqdrJXkXTNLNRMo7ZlHHiuAnA==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.4", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.4" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.4", + "contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.4" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "JPJArwA1kdj8qDAkY2XGjSWoYnqiM7q/3yRNkt6n28Mnn95MuEGkZXUbPBf7qc3IjwrGY5ttQon7yqHZyQJmOQ==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==" + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "MF1CHaRcC+MLFdnDthv4/bKWBZnlnSpkGqa87pKukQefgEdwtb9zFW6zs0GjPp73qtpYYg4q6PEKbzJbxCpKfw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "Microsoft.Win32.Registry": "5.0.0", + "System.CodeDom": "5.0.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.1.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" + }, + "speckle.objects": { + "type": "Project", + "dependencies": { + "Speckle.Sdk": "[1.0.0, )" + } + }, + "speckle.sdk": { + "type": "Project", + "dependencies": { + "GraphQL.Client": "[6.0.0, )", + "Microsoft.CSharp": "[4.7.0, )", + "Microsoft.Data.Sqlite": "[7.0.7, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", + "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", + "Polly": "[7.2.3, )", + "Polly.Contrib.WaitAndRetry": "[1.1.1, )", + "Polly.Extensions.Http": "[3.0.0, )", + "Speckle.DoubleNumerics": "[4.0.1, )", + "Speckle.Newtonsoft.Json": "[13.0.2, )" + } + }, + "speckle.sdk.tests.performance": { + "type": "Project", + "dependencies": { + "BenchmarkDotNet": "[0.14.0, )", + "Microsoft.Extensions.DependencyInjection": "[2.2.0, )", + "Speckle.Objects": "[1.0.0, )" + } + }, + "BenchmarkDotNet": { + "type": "CentralTransitive", + "requested": "[0.14.0, )", + "resolved": "0.14.0", + "contentHash": "eIPSDKi3oni734M1rt/XJAwGQQOIf9gLjRRKKJ0HuVy3vYd7gnmAIX1bTjzI9ZbAY/nPddgqqgM/TeBYitMCIg==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.14.0", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.17.0", + "Microsoft.CodeAnalysis.CSharp": "4.1.0", + "Microsoft.Diagnostics.Runtime": "2.2.332302", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.8", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Perfolizer": "[0.3.17]", + "System.Management": "5.0.0" + } + }, + "GraphQL.Client": { + "type": "CentralTransitive", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==", + "dependencies": { + "GraphQL.Client.Abstractions": "6.0.0", + "GraphQL.Client.Abstractions.Websocket": "6.0.0", + "System.Reactive": "5.0.0" + } + }, + "Microsoft.CSharp": { + "type": "CentralTransitive", + "requested": "[4.7.0, )", + "resolved": "4.7.0", + "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + }, + "Microsoft.Data.Sqlite": { + "type": "CentralTransitive", + "requested": "[7.0.7, )", + "resolved": "7.0.7", + "contentHash": "tiNmV1oPy+Z2R7Wd0bPB/FxCr8B+/5q11OpDMG751GA/YuOL7MZrBFfzv5oFRlFe08K6sjrnbrauzzGIeNrzLQ==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "7.0.7", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.4" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "MZtBIwfDFork5vfjpJdG5g8wuJFt7d/y3LOSVVtDK/76wlbtz6cjltfKHqLx2TKVqTj5/c41t77m1+h20zqtPA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw==" + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "Nxqhadc9FCmFHzU+fz3oc8sFlE6IadViYg8dfUdGzJZ2JUxnCsRghBhhOWdM4B2zSZqEc+0BjliBh/oNdRZuig==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "2.2.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0", + "Microsoft.Extensions.Logging.Abstractions": "2.2.0", + "Microsoft.Extensions.Options": "2.2.0" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, + "Polly": { + "type": "CentralTransitive", + "requested": "[7.2.3, )", + "resolved": "7.2.3", + "contentHash": "DeCY0OFbNdNxsjntr1gTXHJ5pKUwYzp04Er2LLeN3g6pWhffsGuKVfMBLe1lw7x76HrPkLxKEFxBlpRxS2nDEQ==" + }, + "Polly.Contrib.WaitAndRetry": { + "type": "CentralTransitive", + "requested": "[1.1.1, )", + "resolved": "1.1.1", + "contentHash": "1MUQLiSo4KDkQe6nzQRhIU05lm9jlexX5BVsbuw0SL82ynZ+GzAHQxJVDPVBboxV37Po3SG077aX8DuSy8TkaA==" + }, + "Polly.Extensions.Http": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==", + "dependencies": { + "Polly": "7.1.0" + } + }, + "Speckle.DoubleNumerics": { + "type": "CentralTransitive", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "MzEQ1Im0zTja+tEsdRIk/WlPiKqb22NmTOJcR1ZKm/mz46pezyyID3/wRz6vJUELMpSLnG7LhsxBL+nxbr7V0w==" + }, + "Speckle.Newtonsoft.Json": { + "type": "CentralTransitive", + "requested": "[13.0.2, )", + "resolved": "13.0.2", + "contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA==" + } + } + } +} \ No newline at end of file diff --git a/tests/Speckle.Sdk.Serialization.Tests/TestTransport.cs b/tests/Speckle.Sdk.Serialization.Tests/TestTransport.cs index d4c6de37..6002a816 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/TestTransport.cs +++ b/tests/Speckle.Sdk.Serialization.Tests/TestTransport.cs @@ -30,16 +30,17 @@ public string TransportName public void SaveObject(string id, string serializedObject) => Objects[id] = serializedObject; - public Task WriteComplete() => throw new NotImplementedException(); + public ValueTask WriteComplete() => throw new NotImplementedException(); - public Task GetObject(string id) => Task.FromResult(Objects.TryGetValue(id, out string? o) ? o : null); + public ValueTask GetObject(string id) => + ValueTask.FromResult(Objects.TryGetValue(id, out string? o) ? o : null); - public Task CopyObjectAndChildren( + public ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action? onTotalChildrenCountKnown = null ) => throw new NotImplementedException(); - public Task> HasObjects(IReadOnlyList objectIds) => + public ValueTask> HasObjects(IReadOnlyList objectIds) => throw new NotImplementedException(); } diff --git a/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json b/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json index e1d02f86..8d72263b 100644 --- a/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json +++ b/tests/Speckle.Sdk.Serialization.Tests/packages.lock.json @@ -278,6 +278,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -330,6 +332,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )", diff --git a/tests/Speckle.Sdk.Tests.Integration/packages.lock.json b/tests/Speckle.Sdk.Tests.Integration/packages.lock.json index 089dd8e4..55699d02 100644 --- a/tests/Speckle.Sdk.Tests.Integration/packages.lock.json +++ b/tests/Speckle.Sdk.Tests.Integration/packages.lock.json @@ -272,6 +272,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -346,6 +348,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )", diff --git a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs index a14ec73c..1dad2146 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs +++ b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralDeserializerTest.cs @@ -1,5 +1,7 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; using Microsoft.Extensions.Logging.Abstractions; using Speckle.Objects.Geometry; using Speckle.Sdk.Credentials; @@ -12,23 +14,33 @@ namespace Speckle.Sdk.Tests.Performance.Benchmarks; /// /// How many threads on our Deserializer is optimal /// +[Config(typeof(Config))] +[RankColumn] [MemoryDiagnoser] -[SimpleJob(RunStrategy.Monitoring)] public class GeneralDeserializer : IDisposable { private TestDataHelper _dataSource; + private class Config : ManualConfig + { + public Config() + { + var job = Job.ShortRun.WithLaunchCount(0).WithWarmupCount(0).WithIterationCount(1); + AddJob(job); + } + } + [GlobalSetup] public async Task Setup() { TypeLoader.Initialize(typeof(Base).Assembly, typeof(Point).Assembly); + var url = "https://latest.speckle.systems/projects/a3ac1b2706/models/59d3b0f3c6"; //small? + + //var url = "https://latest.speckle.systems/projects/2099ac4b5f/models/da511c4d1e"; //perf? _dataSource = new TestDataHelper(); await _dataSource .SeedTransport( - new Account() - { - serverInfo = new() { url = "https://latest.speckle.systems/projects/2099ac4b5f/models/da511c4d1e" } - }, + new Account() { serverInfo = new() { url = url } }, "2099ac4b5f", "30fb4cbe6eb2202b9e7b4a4fcc3dd2b6" ) @@ -43,6 +55,14 @@ public async Task RunTest() return await sut.DeserializeAsync(data); } + [Benchmark] + public async Task RunTest2() + { + SpeckleObjectDeserializer2 sut = new() { ReadTransport = _dataSource.Transport }; + string data = await _dataSource.Transport.GetObject(_dataSource.ObjectId)!; + return await sut.DeserializeAsync(data); + } + [GlobalCleanup] public void Cleanup() { diff --git a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralSerializerTest.cs b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralSerializerTest.cs index 97a6e6f4..6c4effd8 100644 --- a/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralSerializerTest.cs +++ b/tests/Speckle.Sdk.Tests.Performance/Benchmarks/GeneralSerializerTest.cs @@ -1,5 +1,8 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; using Microsoft.Extensions.Logging.Abstractions; using Speckle.Objects.Geometry; using Speckle.Sdk.Common; @@ -14,23 +17,35 @@ namespace Speckle.Sdk.Tests.Performance.Benchmarks; /// /// How many threads on our Deserializer is optimal /// +[Config(typeof(Config))] +[RankColumn] [MemoryDiagnoser] -[SimpleJob(RunStrategy.Monitoring)] public class GeneralSerializerTest { private Base _testData; + private class Config : ManualConfig + { + public Config() + { + var job = Job.ShortRun.WithLaunchCount(0).WithWarmupCount(0).WithIterationCount(1); + AddJob(job); + } + } + [GlobalSetup] public async Task Setup() { TypeLoader.Initialize(typeof(Base).Assembly, typeof(Point).Assembly); + + var url = "https://latest.speckle.systems/projects/a3ac1b2706/models/59d3b0f3c6"; //small? + + //var url = "https://latest.speckle.systems/projects/2099ac4b5f/models/da511c4d1e"; //perf? + using var dataSource = new TestDataHelper(); await dataSource .SeedTransport( - new Account() - { - serverInfo = new() { url = "https://latest.speckle.systems/projects/2099ac4b5f/models/da511c4d1e" } - }, + new Account() { serverInfo = new() { url = url } }, "2099ac4b5f", "30fb4cbe6eb2202b9e7b4a4fcc3dd2b6" ) @@ -49,6 +64,15 @@ public string RunTest() var x = sut.Serialize(_testData); return x; } + + [Benchmark] + public string RunTest2() + { + var remote = new NullTransport(); + SpeckleObjectSerializer2 sut = new([remote]); + var x = sut.Serialize(_testData); + return x; + } } public class NullTransport : ITransport @@ -65,19 +89,19 @@ public void EndWrite() { } public void SaveObject(string id, string serializedObject) { } - public Task WriteComplete() + public ValueTask WriteComplete() { - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public Task GetObject(string id) => throw new NotImplementedException(); + public ValueTask GetObject(string id) => throw new NotImplementedException(); - public Task CopyObjectAndChildren( + public ValueTask CopyObjectAndChildren( string id, ITransport targetTransport, Action onTotalChildrenCountKnown = null ) => throw new NotImplementedException(); - public Task> HasObjects(IReadOnlyList objectIds) => + public ValueTask> HasObjects(IReadOnlyList objectIds) => throw new NotImplementedException(); } diff --git a/tests/Speckle.Sdk.Tests.Performance/packages.lock.json b/tests/Speckle.Sdk.Tests.Performance/packages.lock.json index e9528b66..57c10730 100644 --- a/tests/Speckle.Sdk.Tests.Performance/packages.lock.json +++ b/tests/Speckle.Sdk.Tests.Performance/packages.lock.json @@ -365,6 +365,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -417,6 +419,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )", diff --git a/tests/Speckle.Sdk.Tests.Unit/Api/Operations/SerializationTests2.cs b/tests/Speckle.Sdk.Tests.Unit/Api/Operations/SerializationTests2.cs new file mode 100644 index 00000000..9dfddb3a --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Unit/Api/Operations/SerializationTests2.cs @@ -0,0 +1,276 @@ +using System.Drawing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Speckle.Sdk.Api; +using Speckle.Sdk.Api.GraphQL.Models; +using Speckle.Sdk.Credentials; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; +using Speckle.Sdk.Tests.Unit.Host; +using Point = Speckle.Sdk.Tests.Unit.Host.Point; + +namespace Speckle.Sdk.Tests.Unit.Api.Operations; + +[TestFixture] +[TestOf(typeof(Sdk.Api.Operations))] +public class ObjectSerialization2 +{ + private IOperations _operations; + + [SetUp] + public void Setup() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(Base).Assembly, typeof(DataChunk).Assembly, typeof(ColorMock2).Assembly); + var serviceProvider = TestServiceSetup.GetServiceProvider(); + _operations = serviceProvider.GetRequiredService(); + } + + [Test] + public async Task IgnoreCircularReferences() + { + var pt = new Point(1, 2, 3); + pt["circle"] = pt; + + var test = _operations.Serialize2(pt); + + var result = await _operations.DeserializeAsync(test); + var circle = result["circle"]; + Assert.That(circle, Is.Null); + } + + [Test] + public async Task InterfacePropHandling() + { + Line tail = new() { Start = new Point(0, 0, 0), End = new Point(42, 42, 42) }; + PolygonalFeline cat = new() { Tail = tail }; + + for (int i = 0; i < 10; i++) + { + cat.Claws[$"Claw number {i}"] = new Line + { + Start = new Point(i, i, i), + End = new Point(i + 3.14, i + 3.14, i + 3.14) + }; + + if (i % 2 == 0) + { + cat.Whiskers.Add( + new Line { Start = new Point(i / 2, i / 2, i / 2), End = new Point(i + 3.14, i + 3.14, i + 3.14) } + ); + } + else + { + var brokenWhisker = new Polyline(); + brokenWhisker.Points.Add(new Point(-i, 0, 0)); + brokenWhisker.Points.Add(new Point(0, 0, 0)); + brokenWhisker.Points.Add(new Point(i, 0, 0)); + cat.Whiskers.Add(brokenWhisker); + } + + cat.Fur[i] = new Line { Start = new Point(i, i, i), End = new Point(i + 3.14, i + 3.14, i + 3.14) }; + } + + var result = _operations.Serialize2(cat); + + var deserialisedFeline = await _operations.DeserializeAsync(result); + + Assert.That(deserialisedFeline.GetId(), Is.EqualTo(cat.GetId())); // If we're getting the same hash... we're probably fine! + } + + [Test] + public async Task InheritanceTests() + { + var superPoint = new SuperPoint + { + X = 10, + Y = 10, + Z = 10, + W = 42 + }; + + var str = _operations.Serialize2(superPoint); + var sstr = await _operations.DeserializeAsync(str); + + Assert.That(sstr.speckle_type, Is.EqualTo(superPoint.speckle_type)); + } + + [Test] + public async Task ListDynamicProp() + { + var point = new Point(); + var test = new List(); + + for (var i = 0; i < 100; i++) + { + test.Add(new SuperPoint { W = i }); + } + + point["test"] = test; + + var str = _operations.Serialize2(point); + var dsrls = await _operations.DeserializeAsync(str); + + var list = dsrls["test"] as List; // NOTE: on dynamically added lists, we cannot infer the inner type and we always fall back to a generic list. + Assert.That(list, Has.Count.EqualTo(100)); + } + + [Test] + public async Task ChunkSerialisation() + { + var baseBasedChunk = new DataChunk() { data = new() }; + for (var i = 0; i < 200; i++) + { + baseBasedChunk.data.Add(new SuperPoint { W = i }); + } + + var stringBasedChunk = new DataChunk() { data = new() }; + for (var i = 0; i < 200; i++) + { + stringBasedChunk.data.Add(i + "_hai"); + } + + var doubleBasedChunk = new DataChunk() { data = new() }; + for (var i = 0; i < 200; i++) + { + doubleBasedChunk.data.Add(i + 0.33); + } + + var baseChunkString = _operations.Serialize2(baseBasedChunk); + var stringChunkString = _operations.Serialize2(stringBasedChunk); + var doubleChunkString = _operations.Serialize2(doubleBasedChunk); + + var baseChunkDeserialised = (DataChunk)await _operations.DeserializeAsync(baseChunkString); + var stringChunkDeserialised = (DataChunk)await _operations.DeserializeAsync(stringChunkString); + var doubleChunkDeserialised = (DataChunk)await _operations.DeserializeAsync(doubleChunkString); + + Assert.That(baseChunkDeserialised.data, Has.Count.EqualTo(baseBasedChunk.data.Count)); + Assert.That(stringChunkDeserialised.data, Has.Count.EqualTo(stringBasedChunk.data.Count)); + Assert.That(doubleChunkDeserialised.data, Has.Count.EqualTo(doubleBasedChunk.data.Count)); + } + + [Test] + public async Task ObjectWithChunksSerialisation() + { + const int MAX_NUM = 2020; + var mesh = new FakeMesh { ArrayOfDoubles = new double[MAX_NUM], ArrayOfLegs = new TableLeg[MAX_NUM] }; + + var customChunk = new List(); + var defaultChunk = new List(); + + for (int i = 0; i < MAX_NUM; i++) + { + mesh.Vertices.Add(i / 2); + customChunk.Add(i / 2); + defaultChunk.Add(i / 2); + mesh.Tables.Add(new Tabletop { length = 2000 }); + mesh.ArrayOfDoubles[i] = i * 3.3; + mesh.ArrayOfLegs[i] = new TableLeg { height = 2 + i }; + } + + mesh["@(800)CustomChunk"] = customChunk; + mesh["@()DefaultChunk"] = defaultChunk; + + var serialised = _operations.Serialize2(mesh); + var deserialised = await _operations.DeserializeAsync(serialised); + + Assert.That(mesh.GetId(), Is.EqualTo(deserialised.GetId())); + } + + [Test] + public void EmptyListSerialisationTests() + { + // NOTE: expected behaviour is that empty lists should serialize as empty lists. Don't ask why, it's complicated. + // Regarding chunkable empty lists, to prevent empty chunks, the expected behaviour is to have an empty lists, with no chunks inside. + var test = new Base(); + + test["@(5)emptyChunks"] = new List(); + test["emptyList"] = new List(); + test["@emptyDetachableList"] = new List(); + + // Note: nested empty lists should be preserved. + test["nestedList"] = new List { new List { new List() } }; + test["@nestedDetachableList"] = new List { new List { new List() } }; + + var serialised = _operations.Serialize2(test); + var isCorrect = + serialised.Contains("\"@(5)emptyChunks\":[]") + && serialised.Contains("\"emptyList\":[]") + && serialised.Contains("\"@emptyDetachableList\":[]") + && serialised.Contains("\"nestedList\":[[[]]]") + && serialised.Contains("\"@nestedDetachableList\":[[[]]]"); + + Assert.That(isCorrect, Is.EqualTo(true)); + } + + [SpeckleType("Speckle.Core.Tests.Unit.Api.Operations.ObjectSerialization+DateMock2")] + private class DateMock2 : Base + { + public DateTime TestField { get; set; } + } + + [Test] + public async Task DateSerialisation() + { + var date = new DateTime(2020, 1, 14); + var mockBase = new DateMock2 { TestField = date }; + + var result = _operations.Serialize2(mockBase); + var test = (DateMock2)await _operations.DeserializeAsync(result); + + Assert.That(test.TestField, Is.EqualTo(date)); + } + + [SpeckleType("Speckle.Core.Tests.Unit.Api.Operations.ObjectSerialization+GUIDMock2")] + private class GUIDMock2 : Base + { + public Guid TestField { get; set; } + } + + [Test] + public async Task GuidSerialisation() + { + var guid = Guid.NewGuid(); + var mockBase = new GUIDMock2 { TestField = guid }; + + var result = _operations.Serialize2(mockBase); + var test = (GUIDMock2)await _operations.DeserializeAsync(result); + + Assert.That(test.TestField, Is.EqualTo(guid)); + } + + [SpeckleType("Speckle.Core.Tests.Unit.Api.Operations.ObjectSerialization+ColorMock2")] + private class ColorMock2 : Base + { + public Color TestField { get; set; } + } + + [Test] + public async Task ColorSerialisation() + { + var color = Color.FromArgb(255, 4, 126, 251); + var mockBase = new ColorMock2 { TestField = color }; + + var result = _operations.Serialize2(mockBase); + var test = (ColorMock2)await _operations.DeserializeAsync(result); + + Assert.That(test.TestField, Is.EqualTo(color)); + } + + [SpeckleType("Speckle.Core.Tests.Unit.Api.Operations.ObjectSerialization+StringDateTimeRegressionMock2")] + private class StringDateTimeRegressionMock2 : Base + { + public string TestField { get; set; } + } + + [Test] + public async Task StringDateTimeRegression() + { + var mockBase = new StringDateTimeRegressionMock2 { TestField = "2021-11-12T11:32:01" }; + + var result = _operations.Serialize2(mockBase); + var test = (StringDateTimeRegressionMock2)await _operations.DeserializeAsync(result); + + Assert.That(test.TestField, Is.EqualTo(mockBase.TestField)); + } +} diff --git a/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializerNonBreakingChanges2.cs b/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializerNonBreakingChanges2.cs new file mode 100644 index 00000000..302f3f53 --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Unit/Serialisation/SerializerNonBreakingChanges2.cs @@ -0,0 +1,311 @@ +using System.Drawing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Shouldly; +using Speckle.Sdk.Api; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation; +using Matrix4x4 = Speckle.DoubleNumerics.Matrix4x4; + +namespace Speckle.Sdk.Tests.Unit.Serialisation; + +/// +/// Test fixture that documents what property typing changes maintain backwards/cross/forwards compatibility, and are "non-breaking" changes. +/// This doesn't guarantee things work this way for SpecklePy +/// Nor does it encompass other tricks (like deserialize callback, or computed json ignored properties) +/// +[TestFixture] +[Description("For certain types, changing property from one type to another should be implicitly backwards compatible")] +public class SerializerNonBreakingChanges2 : PrimitiveTestFixture2 +{ + private IOperations _operations; + + [SetUp] + public void Setup() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(StringValueMock).Assembly); + var serviceProvider = TestServiceSetup.GetServiceProvider(); + _operations = serviceProvider.GetRequiredService(); + } + + [Test, TestCaseSource(nameof(Int8TestCases)), TestCaseSource(nameof(Int32TestCases))] + public async Task IntToColor(int argb) + { + var from = new IntValueMock { value = argb }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value.ToArgb(), Is.EqualTo(argb)); + } + + [Test, TestCaseSource(nameof(Int8TestCases)), TestCaseSource(nameof(Int32TestCases))] + public async Task ColorToInt(int argb) + { + var from = new ColorValueMock { value = Color.FromArgb(argb) }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(argb)); + } + + [ + Test, + TestCaseSource(nameof(Int8TestCases)), + TestCaseSource(nameof(Int32TestCases)), + TestCaseSource(nameof(Int64TestCases)) + ] + public async Task IntToDouble(long testCase) + { + var from = new IntValueMock { value = testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(testCase)); + } + + [Test] + public async Task NullToInt() + { + var from = new ObjectValueMock { value = null }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(default(int))); + } + + [Test] + public async Task NullToDouble() + { + var from = new ObjectValueMock { value = null }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(default(double))); + } + + // IMPORTANT!!: This test mimics large numbers that we sometimes see from python + // This is behaviour our deserializer has, but not necessarily commited to keeping + // Numbers outside the range of a Long are not officially supported + [Test] + [TestCaseSource(nameof(UInt64TestCases))] + [DefaultFloatingPointTolerance(2048)] + public async Task UIntToDouble(ulong testCase) + { + var from = new UIntValueMock { value = testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(testCase)); + } + + [ + Test, + TestCaseSource(nameof(Int8TestCases)), + TestCaseSource(nameof(Int32TestCases)), + TestCaseSource(nameof(Int64TestCases)) + ] + public async Task IntToString(long testCase) + { + var from = new IntValueMock { value = testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(testCase.ToString())); + } + + private static readonly double[][] s_arrayTestCases = + { + Array.Empty(), + new double[] { 0, 1, int.MaxValue, int.MinValue }, + new[] { default, double.Epsilon, double.MaxValue, double.MinValue } + }; + + [Test, TestCaseSource(nameof(s_arrayTestCases))] + public async Task ArrayToList(double[] testCase) + { + var from = new ArrayDoubleValueMock { value = testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EquivalentTo(testCase)); + } + + [Test, TestCaseSource(nameof(s_arrayTestCases))] + public async Task ListToArray(double[] testCase) + { + var from = new ListDoubleValueMock { value = testCase.ToList() }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EquivalentTo(testCase)); + } + + [Test, TestCaseSource(nameof(s_arrayTestCases))] + public async Task ListToIList(double[] testCase) + { + var from = new ListDoubleValueMock { value = testCase.ToList() }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EquivalentTo(testCase)); + } + + [Test, TestCaseSource(nameof(s_arrayTestCases))] + public async Task ListToIReadOnlyList(double[] testCase) + { + var from = new ListDoubleValueMock { value = testCase.ToList() }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EquivalentTo(testCase)); + } + + [Test, TestCaseSource(nameof(s_arrayTestCases))] + public async Task IListToList(double[] testCase) + { + var from = new IListDoubleValueMock { value = testCase.ToList() }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EquivalentTo(testCase)); + } + + [Test, TestCaseSource(nameof(s_arrayTestCases))] + public async Task IReadOnlyListToList(double[] testCase) + { + var from = new IReadOnlyListDoubleValueMock { value = testCase.ToList() }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EquivalentTo(testCase)); + } + + [Test, TestCaseSource(nameof(MyEnums))] + public async Task EnumToInt(MyEnum testCase) + { + var from = new EnumValueMock { value = testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo((int)testCase)); + } + + [Test, TestCaseSource(nameof(MyEnums))] + public async Task IntToEnum(MyEnum testCase) + { + var from = new IntValueMock { value = (int)testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(testCase)); + } + + [Test] + [TestCaseSource(nameof(Float64TestCases))] + [TestCaseSource(nameof(Float32TestCases))] + public async Task DoubleToDouble(double testCase) + { + var from = new DoubleValueMock { value = testCase }; + + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value, Is.EqualTo(testCase)); + } + + [Test] + [TestCase(123, 255)] + [TestCase(256, 1)] + [TestCase(256, float.MinValue)] + public async Task ListToMatrix64(int seed, double scalar) + { + Random rand = new(seed); + List testCase = Enumerable.Range(0, 16).Select(_ => rand.NextDouble() * scalar).ToList(); + + ListDoubleValueMock from = new() { value = testCase, }; + + //Test List -> Matrix + var res = await from.SerializeAsTAndDeserialize(_operations); + Assert.That(res.value.M11, Is.EqualTo(testCase[0])); + Assert.That(res.value.M44, Is.EqualTo(testCase[testCase.Count - 1])); + + //Test Matrix -> List + var backAgain = await res.SerializeAsTAndDeserialize(_operations); + Assert.That(backAgain.value, Is.Not.Null); + Assert.That(backAgain.value, Is.EquivalentTo(testCase)); + } + + [Test] + [TestCase(123, 255)] + [TestCase(256, 1)] + [DefaultFloatingPointTolerance(Constants.EPS)] + public void Matrix32ToMatrix64(int seed, float scalar) + { + Random rand = new(seed); + List testCase = Enumerable.Range(0, 16).Select(_ => rand.NextDouble() * scalar).ToList(); + + ListDoubleValueMock from = new() { value = testCase }; + + //Test List -> Matrix + var exception = Assert.ThrowsAsync( + async () => await from.SerializeAsTAndDeserialize(_operations) + ); + exception.ShouldNotBeNull(); + } +} + +public abstract class SerializerMock2 : Base +{ + private string _speckle_type; + + protected SerializerMock2() + { + _speckle_type = base.speckle_type; + } + + public override string speckle_type => _speckle_type; + + public void SerializeAs() + where T : Base, new() + { + T target = new(); + _speckle_type = target.speckle_type; + } + + internal async Task SerializeAsTAndDeserialize(IOperations operations) + where TTo : Base, new() + { + SerializeAs(); + + var json = operations.Serialize2(this); + + Base result = await operations.DeserializeAsync(json); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.TypeOf()); + return (TTo)result; + } +} + +public abstract class PrimitiveTestFixture2 +{ + public static readonly sbyte[] Int8TestCases = { default, sbyte.MaxValue, sbyte.MinValue }; + public static readonly short[] Int16TestCases = { short.MaxValue, short.MinValue }; + public static readonly int[] Int32TestCases = { int.MinValue, int.MaxValue }; + public static readonly long[] Int64TestCases = { long.MaxValue, long.MinValue }; + public static readonly ulong[] UInt64TestCases = { ulong.MaxValue, ulong.MinValue }; + + public static double[] Float64TestCases { get; } = + { + default, + double.Epsilon, + double.MaxValue, + double.MinValue, + double.PositiveInfinity, + double.NegativeInfinity, + double.NaN + }; + + public static float[] Float32TestCases { get; } = + { + default, + float.Epsilon, + float.MaxValue, + float.MinValue, + float.PositiveInfinity, + float.NegativeInfinity, + float.NaN + }; + + public static Half[] Float16TestCases { get; } = + { default, Half.Epsilon, Half.MaxValue, Half.MinValue, Half.PositiveInfinity, Half.NegativeInfinity, Half.NaN }; + + public static float[] FloatIntegralTestCases { get; } = { 0, 1, int.MaxValue, int.MinValue }; + + public static MyEnum[] MyEnums { get; } = Enum.GetValues(typeof(MyEnum)).Cast().ToArray(); +} diff --git a/tests/Speckle.Sdk.Tests.Unit/Serialisation/SimpleRoundTripTests2.cs b/tests/Speckle.Sdk.Tests.Unit/Serialisation/SimpleRoundTripTests2.cs new file mode 100644 index 00000000..3c537bd4 --- /dev/null +++ b/tests/Speckle.Sdk.Tests.Unit/Serialisation/SimpleRoundTripTests2.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Speckle.Sdk.Api; +using Speckle.Sdk.Host; +using Speckle.Sdk.Models; +using Speckle.Sdk.Tests.Unit.Host; + +namespace Speckle.Sdk.Tests.Unit.Serialisation; + +public class SimpleRoundTripTests2 +{ + private IOperations _operations; + + static SimpleRoundTripTests2() + { + Reset(); + } + + private static void Reset() + { + TypeLoader.Reset(); + TypeLoader.Initialize(typeof(Base).Assembly, Assembly.GetExecutingAssembly()); + } + + public static IEnumerable TestData() + { + yield return new DiningTable { ["@strangeVariable_NAme3"] = new TableLegFixture() }; + + var polyline = new Polyline(); + for (int i = 0; i < 100; i++) + { + polyline.Points.Add(new Point { X = i * 2, Y = i % 2 }); + } + yield return polyline; + } + + [SetUp] + public void Setup() + { + Reset(); + + var serviceProvider = TestServiceSetup.GetServiceProvider(); + _operations = serviceProvider.GetRequiredService(); + } + + [TestCaseSource(nameof(TestData))] + public async Task SimpleSerialization(Base testData) + { + var result = _operations.Serialize2(testData); + var test = await _operations.DeserializeAsync(result); + + Assert.That(testData.GetId(), Is.EqualTo(test.GetId())); + } +} diff --git a/tests/Speckle.Sdk.Tests.Unit/packages.lock.json b/tests/Speckle.Sdk.Tests.Unit/packages.lock.json index e1755b70..8e774e98 100644 --- a/tests/Speckle.Sdk.Tests.Unit/packages.lock.json +++ b/tests/Speckle.Sdk.Tests.Unit/packages.lock.json @@ -287,6 +287,8 @@ "Microsoft.Data.Sqlite": "[7.0.7, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", + "Microsoft.Extensions.ObjectPool": "[8.0.8, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.1, )", "Polly": "[7.2.3, )", "Polly.Contrib.WaitAndRetry": "[1.1.1, )", "Polly.Extensions.Http": "[3.0.0, )", @@ -339,6 +341,18 @@ "Microsoft.Extensions.Options": "2.2.0" } }, + "Microsoft.Extensions.ObjectPool": { + "type": "CentralTransitive", + "requested": "[8.0.8, )", + "resolved": "8.0.8", + "contentHash": "wnjTFjEvvSbOs3iMfl6CeJcUgPHZMYUB9uAQbGQGxGwVRl4GydNpMSkVntTzoi7AqQeYumU9yDSNeVbpq+ebow==" + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "s/s20YTVY9r9TPfTrN5g8zPF1YhwxyqO6PxUkrYTGI2B+OGPe9AdajWZrLhFqXIvqIW23fnUE4+ztrUWNU1+9g==" + }, "Polly": { "type": "CentralTransitive", "requested": "[7.2.3, )",